diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 36717b4858e5..000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,17 +0,0 @@ - -**Affects:** \ - ---- - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..8d92ceeb6f96 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: + - name: Community Support + url: https://stackoverflow.com/tags/spring + about: Please ask and answer questions on StackOverflow with the tag `spring`. + diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md new file mode 100644 index 000000000000..08396dcc717e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -0,0 +1,23 @@ +--- +name: General +about: Bugs, enhancements, documentation, tasks. +title: '' +labels: '' +assignees: '' + +--- + + + diff --git a/.github/actions/await-http-resource/action.yml b/.github/actions/await-http-resource/action.yml new file mode 100644 index 000000000000..ba177fb757b5 --- /dev/null +++ b/.github/actions/await-http-resource/action.yml @@ -0,0 +1,20 @@ +name: Await HTTP Resource +description: 'Waits for an HTTP resource to be available (a HEAD request succeeds)' +inputs: + url: + description: 'URL of the resource to await' + required: true +runs: + using: composite + steps: + - name: Await HTTP resource + shell: bash + run: | + url=${{ inputs.url }} + echo "Waiting for $url" + until curl --fail --head --silent ${{ inputs.url }} > /dev/null + do + echo "." + sleep 60 + done + echo "$url is available" diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 000000000000..126d25b6ba8a --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,61 @@ +name: 'Build' +description: 'Builds the project, optionally publishing it to a local deployment repository' +inputs: + develocity-access-key: + description: 'Access key for authentication with ge.spring.io' + required: false + java-distribution: + description: 'Java distribution to use' + required: false + default: 'liberica' + java-early-access: + description: 'Whether the Java version is in early access' + required: false + default: 'false' + java-toolchain: + description: 'Whether a Java toolchain should be used' + required: false + default: 'false' + java-version: + description: 'Java version to compile and test with' + required: false + default: '17' + publish: + description: 'Whether to publish artifacts ready for deployment to Artifactory' + required: false + default: 'false' +outputs: + build-scan-url: + description: 'URL, if any, of the build scan produced by the build' + value: ${{ (inputs.publish == 'true' && steps.publish.outputs.build-scan-url) || steps.build.outputs.build-scan-url }} + version: + description: 'Version that was built' + value: ${{ steps.read-version.outputs.version }} +runs: + using: composite + steps: + - name: Prepare Gradle Build + uses: ./.github/actions/prepare-gradle-build + with: + develocity-access-key: ${{ inputs.develocity-access-key }} + java-distribution: ${{ inputs.java-distribution }} + java-early-access: ${{ inputs.java-early-access }} + java-toolchain: ${{ inputs.java-toolchain }} + java-version: ${{ inputs.java-version }} + - name: Build + id: build + if: ${{ inputs.publish == 'false' }} + shell: bash + run: ./gradlew check antora + - name: Publish + id: publish + if: ${{ inputs.publish == 'true' }} + shell: bash + run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository + - name: Read Version From gradle.properties + id: read-version + shell: bash + run: | + version=$(sed -n 's/version=\(.*\)/\1/p' gradle.properties) + echo "Version is $version" + echo "version=$version" >> $GITHUB_OUTPUT diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml new file mode 100644 index 000000000000..03452537adf8 --- /dev/null +++ b/.github/actions/create-github-release/action.yml @@ -0,0 +1,27 @@ +name: Create GitHub Release +description: 'Create the release on GitHub with a changelog' +inputs: + milestone: + description: 'Name of the GitHub milestone for which a release will be created' + required: true + pre-release: + description: 'Whether the release is a pre-release (a milestone or release candidate)' + required: false + default: 'false' + token: + description: 'Token to use for authentication with GitHub' + required: true +runs: + using: composite + steps: + - name: Generate Changelog + uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 + with: + config-file: .github/actions/create-github-release/changelog-generator.yml + milestone: ${{ inputs.milestone }} + token: ${{ inputs.token }} + - name: Create GitHub Release + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.token }} + run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md ${{ inputs.pre-release == 'true' && '--prerelease' || '' }} diff --git a/.github/actions/create-github-release/changelog-generator.yml b/.github/actions/create-github-release/changelog-generator.yml new file mode 100644 index 000000000000..725c40966679 --- /dev/null +++ b/.github/actions/create-github-release/changelog-generator.yml @@ -0,0 +1,28 @@ +changelog: + repository: spring-projects/spring-framework + sections: + - title: ":star: New Features" + labels: + - "type: enhancement" + - title: ":lady_beetle: Bug Fixes" + labels: + - "type: bug" + - "type: regression" + - title: ":notebook_with_decorative_cover: Documentation" + labels: + - "type: documentation" + - title: ":hammer: Dependency Upgrades" + sort: "title" + labels: + - "type: dependency-upgrade" + contributors: + exclude: + names: + - "bclozel" + - "jhoeller" + - "poutsma" + - "rstoyanchev" + - "sbrannen" + - "sdeleuze" + - "simonbasle" + - "snicoll" diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml new file mode 100644 index 000000000000..9b305a81790c --- /dev/null +++ b/.github/actions/prepare-gradle-build/action.yml @@ -0,0 +1,54 @@ +name: Prepare Gradle Build +description: 'Prepares a Gradle build. Sets up Java and Gradle and configures Gradle properties' +inputs: + develocity-access-key: + description: 'Access key for authentication with ge.spring.io' + required: false + java-distribution: + description: 'Java distribution to use' + required: false + default: 'liberica' + java-early-access: + description: 'Whether the Java version is in early access. When true, forces java-distribution to temurin' + required: false + default: 'false' + java-toolchain: + description: 'Whether a Java toolchain should be used' + required: false + default: 'false' + java-version: + description: 'Java version to use for the build' + required: false + default: '17' +runs: + using: composite + steps: + - name: Set Up Java + uses: actions/setup-java@v4 + with: + distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || (inputs.java-distribution || 'liberica') }} + java-version: | + ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} + ${{ inputs.java-toolchain == 'true' && '17' || '' }} + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 + with: + cache-read-only: false + develocity-access-key: ${{ inputs.develocity-access-key }} + develocity-token-expiry: 4 + - name: Configure Gradle Properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties + echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties + - name: Configure Toolchain Properties + if: ${{ inputs.java-toolchain == 'true' }} + shell: bash + run: | + echo toolchainVersion=${{ inputs.java-version }} >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $HOME/.gradle/gradle.properties diff --git a/.github/actions/print-jvm-thread-dumps/action.yml b/.github/actions/print-jvm-thread-dumps/action.yml new file mode 100644 index 000000000000..bcaebf3676aa --- /dev/null +++ b/.github/actions/print-jvm-thread-dumps/action.yml @@ -0,0 +1,17 @@ +name: Print JVM thread dumps +description: 'Prints a thread dump for all running JVMs' +runs: + using: composite + steps: + - if: ${{ runner.os == 'Linux' }} + shell: bash + run: | + for jvm_pid in $(jps -q -J-XX:+PerfDisableSharedMem); do + jcmd $jvm_pid Thread.print + done + - if: ${{ runner.os == 'Windows' }} + shell: powershell + run: | + foreach ($jvm_pid in $(jps -q -J-XX:+PerfDisableSharedMem)) { + jcmd $jvm_pid Thread.print + } diff --git a/.github/actions/send-notification/action.yml b/.github/actions/send-notification/action.yml new file mode 100644 index 000000000000..b379e67897d1 --- /dev/null +++ b/.github/actions/send-notification/action.yml @@ -0,0 +1,39 @@ +name: Send Notification +description: 'Sends a Google Chat message as a notification of the job''s outcome' +inputs: + build-scan-url: + description: 'URL of the build scan to include in the notification' + required: false + run-name: + description: 'Name of the run to include in the notification' + required: false + default: ${{ format('{0} {1}', github.ref_name, github.job) }} + status: + description: 'Status of the job' + required: true + webhook-url: + description: 'Google Chat Webhook URL' + required: true +runs: + using: composite + steps: + - name: Prepare Variables + shell: bash + run: | + echo "BUILD_SCAN=${{ inputs.build-scan-url == '' && ' [build scan unavailable]' || format(' [<{0}|Build Scan>]', inputs.build-scan-url) }}" >> "$GITHUB_ENV" + echo "RUN_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_ENV" + - name: Success Notification + if: ${{ inputs.status == 'success' }} + shell: bash + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was successful ${{ env.BUILD_SCAN }}"}' || true + - name: Failure Notification + if: ${{ inputs.status == 'failure' }} + shell: bash + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: " *<${{ env.RUN_URL }}|${{ inputs.run-name }}> failed* ${{ env.BUILD_SCAN }}"}' || true + - name: Cancel Notification + if: ${{ inputs.status == 'cancelled' }} + shell: bash + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was cancelled"}' || true diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml new file mode 100644 index 000000000000..22fa7fe87f01 --- /dev/null +++ b/.github/actions/sync-to-maven-central/action.yml @@ -0,0 +1,43 @@ +name: Sync to Maven Central +description: 'Syncs a release to Maven Central and waits for it to be available for use' +inputs: + jfrog-cli-config-token: + description: 'Config token for the JFrog CLI' + required: true + ossrh-s01-staging-profile: + description: 'Staging profile to use when syncing to Central' + required: true + ossrh-s01-token-password: + description: 'Password for authentication with s01.oss.sonatype.org' + required: true + ossrh-s01-token-username: + description: 'Username for authentication with s01.oss.sonatype.org' + required: true + spring-framework-version: + description: 'Version of Spring Framework that is being synced to Central' + required: true +runs: + using: composite + steps: + - name: Set Up JFrog CLI + uses: jfrog/setup-jfrog-cli@f748a0599171a192a2668afee8d0497f7c1069df # v4.5.6 + env: + JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }} + - name: Download Release Artifacts + shell: bash + run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}' + - name: Sync + uses: spring-io/nexus-sync-action@42477a2230a2f694f9eaa4643fa9e76b99b7ab84 # v0.0.1 + with: + close: true + create: true + generate-checksums: true + password: ${{ inputs.ossrh-s01-token-password }} + release: true + staging-profile-name: ${{ inputs.ossrh-s01-staging-profile }} + upload: true + username: ${{ inputs.ossrh-s01-token-username }} + - name: Await + uses: ./.github/actions/await-http-resource + with: + url: ${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }} diff --git a/.github/actions/sync-to-maven-central/artifacts.spec b/.github/actions/sync-to-maven-central/artifacts.spec new file mode 100644 index 000000000000..f5c16ed5137a --- /dev/null +++ b/.github/actions/sync-to-maven-central/artifacts.spec @@ -0,0 +1,20 @@ +{ + "files": [ + { + "aql": { + "items.find": { + "$and": [ + { + "@build.name": "${buildName}", + "@build.number": "${buildNumber}", + "path": { + "$nmatch": "org/springframework/framework-api/*" + } + } + ] + } + }, + "target": "nexus/" + } + ] +} 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/backport-bot.yml b/.github/workflows/backport-bot.yml index 153c63247652..db40e9f94ea9 100644 --- a/.github/workflows/backport-bot.yml +++ b/.github/workflows/backport-bot.yml @@ -1,5 +1,4 @@ name: Backport Bot - on: issues: types: [labeled] @@ -18,15 +17,17 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - name: Check out code + uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '17' + distribution: 'liberica' + java-version: 17 - name: Download BackportBot run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar - name: Backport env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_EVENT: ${{ toJSON(github.event) }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT" diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml new file mode 100644 index 000000000000..93cf0f12c168 --- /dev/null +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -0,0 +1,58 @@ +name: Build and Deploy Snapshot +on: + push: + branches: + - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-deploy-snapshot: + name: Build and Deploy Snapshot + if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ubuntu-latest + timeout-minutes: 60 + 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: Deploy + uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2 + 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: 'spring-framework-7.0.x' + folder: 'deployment-repository' + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository: 'libs-snapshot-local' + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + - name: Send Notification + if: always() + uses: ./.github/actions/send-notification + with: + build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }} + run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + outputs: + version: ${{ steps.build-and-publish.outputs.version }} + verify: + name: Verify + needs: build-and-deploy-snapshot + 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: + version: ${{ needs.build-and-deploy-snapshot.outputs.version }} diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml new file mode 100644 index 000000000000..325268f1dd1a --- /dev/null +++ b/.github/workflows/build-pull-request.yml @@ -0,0 +1,25 @@ +name: Build Pull Request +on: pull_request +permissions: + contents: read +jobs: + build: + name: Build Pull Request + if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build + id: build + uses: ./.github/actions/build + - name: Print JVM Thread Dumps When Cancelled + if: cancelled() + uses: ./.github/actions/print-jvm-thread-dumps + - name: Upload Build Reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: build-reports + path: '**/build/reports/' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000000..203fd5ea6f1f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI +on: + schedule: + - cron: '30 9 * * *' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + ci: + name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' + if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ${{ matrix.os.id }} + timeout-minutes: 60 + strategy: + matrix: + os: + - id: ubuntu-latest + name: Linux + java: + - version: 17 + toolchain: false + - version: 21 + toolchain: true + - version: 23 + toolchain: true + exclude: + - os: + name: Linux + java: + version: 17 + steps: + - name: Prepare Windows runner + if: ${{ runner.os == 'Windows' }} + run: | + git config --global core.autocrlf true + git config --global core.longPaths true + Stop-Service -name Docker + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build + id: build + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + java-early-access: ${{ matrix.java.early-access || 'false' }} + java-distribution: ${{ matrix.java.distribution }} + java-toolchain: ${{ matrix.java.toolchain }} + java-version: ${{ matrix.java.version }} + - name: Send Notification + if: always() + uses: ./.github/actions/send-notification + with: + build-scan-url: ${{ steps.build.outputs.build-scan-url }} + run-name: ${{ format('{0} | {1} | Java {2}', github.ref_name, matrix.os.name, matrix.java.version) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f3e899c4fe0e..ea6006f52fc5 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,25 +1,28 @@ name: Deploy Docs on: push: - branches-ignore: [ gh-pages ] - tags: '**' + branches: + - 'main' + - '*.x' + - '!gh-pages' + tags: + - 'v*' repository_dispatch: types: request-build-reference # legacy - schedule: - - cron: '0 10 * * *' # Once per day at 10am UTC workflow_dispatch: permissions: actions: write jobs: build: - runs-on: ubuntu-latest + name: Dispatch docs deployment if: github.repository_owner == 'spring-projects' + runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 + - name: Check out code + uses: actions/checkout@v4 with: - ref: docs-build fetch-depth: 1 + ref: docs-build - name: Dispatch (partial build) if: github.ref_type == 'branch' env: diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml deleted file mode 100644 index c80a7e5278d0..000000000000 --- a/.github/workflows/gradle-wrapper-validation.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "Validate Gradle Wrapper" -on: [push, pull_request] - -permissions: - contents: read - -jobs: - validation: - name: "Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 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 new file mode 100644 index 000000000000..f6b42a55a68f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release +on: + push: + tags: + - v7.0.[0-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@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2 + 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-release-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 }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/.github/workflows/update-antora-ui-spring.yml b/.github/workflows/update-antora-ui-spring.yml new file mode 100644 index 000000000000..b1bc0e7b9cb3 --- /dev/null +++ b/.github/workflows/update-antora-ui-spring.yml @@ -0,0 +1,37 @@ +name: Update Antora UI Spring + +on: + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: + +permissions: + pull-requests: write + issues: write + contents: write + +jobs: + update-antora-ui-spring: + name: Update on Supported Branches + if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ubuntu-latest + strategy: + matrix: + branch: [ '6.1.x' ] + steps: + - uses: spring-io/spring-doc-actions/update-antora-spring-ui@5a57bcc6a0da2a1474136cf29571b277850432bc + name: Update + with: + docs-branch: ${{ matrix.branch }} + token: ${{ secrets.GITHUB_TOKEN }} + antora-file-path: 'framework-docs/antora-playbook.yml' + update-antora-ui-spring-docs-build: + name: Update on docs-build + if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ubuntu-latest + steps: + - uses: spring-io/spring-doc-actions/update-antora-spring-ui@5a57bcc6a0da2a1474136cf29571b277850432bc + name: Update + with: + docs-branch: 'docs-build' + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 000000000000..3ffb330e72ed --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,77 @@ +name: Verify +on: + workflow_call: + inputs: + staging: + description: 'Whether the release to verify is in the staging repository' + required: false + default: false + type: boolean + version: + description: 'Version to verify' + required: true + type: string + secrets: + google-chat-webhook-url: + description: 'Google Chat Webhook URL' + required: true + repository-password: + description: 'Password for authentication with the repository' + required: false + repository-username: + description: 'Username for authentication with the repository' + required: false + token: + description: 'Token to use for authentication with GitHub' + required: true +jobs: + verify: + name: Verify + runs-on: ubuntu-latest + steps: + - name: Check Out Release Verification Tests + uses: actions/checkout@v4 + with: + ref: 'v0.0.2' + repository: spring-projects/spring-framework-release-verification + token: ${{ secrets.token }} + - name: Check Out Send Notification Action + uses: actions/checkout@v4 + with: + path: send-notification + sparse-checkout: .github/actions/send-notification + - name: Set Up Java + uses: actions/setup-java@v4 + with: + distribution: 'liberica' + java-version: 17 + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 + with: + cache-read-only: false + - name: Configure Gradle Properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + - name: Run Release Verification Tests + env: + RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.repository-password }} + RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.repository-username }} + RVT_RELEASE_TYPE: oss + RVT_STAGING: ${{ inputs.staging }} + RVT_VERSION: ${{ inputs.version }} + run: ./gradlew spring-framework-release-verification-tests:test + - name: Upload Build Reports on Failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: build-reports + path: '**/build/reports/' + - name: Send Notification + if: failure() + uses: ./send-notification/.github/actions/send-notification + with: + run-name: ${{ format('{0} | Verification | {1}', github.ref_name, inputs.version) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.google-chat-webhook-url }} diff --git a/.gitignore b/.gitignore index 14252754d456..9a2c0d2c9af6 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,8 @@ spring-*/src/main/java/META-INF/MANIFEST.MF *.iml *.ipr *.iws -.idea +.idea/* +!.idea/icon.svg out test-output atlassian-ide-plugin.xml @@ -51,3 +52,6 @@ atlassian-ide-plugin.xml .vscode/ cached-antora-playbook.yml + +node_modules +/.kotlin/ diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 000000000000..89da1f709357 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,52 @@ + + + + +icon-framework + + + + + + diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 0d65c926d2d5..000000000000 --- a/.mailmap +++ /dev/null @@ -1,36 +0,0 @@ -Juergen Hoeller -Juergen Hoeller -Juergen Hoeller -Rossen Stoyanchev -Rossen Stoyanchev -Rossen Stoyanchev -Phillip Webb -Phillip Webb -Phillip Webb -Chris Beams -Chris Beams -Chris Beams -Arjen Poutsma -Arjen Poutsma -Arjen Poutsma -Arjen Poutsma -Arjen Poutsma -Oliver Drotbohm -Oliver Drotbohm -Oliver Drotbohm -Oliver Drotbohm -Dave Syer -Dave Syer -Dave Syer -Dave Syer -Andy Clement -Andy Clement -Andy Clement -Andy Clement -Sam Brannen -Sam Brannen -Sam Brannen -Simon Basle -Simon Baslé - -Nick Williams diff --git a/.sdkmanrc b/.sdkmanrc index 3c4d046f46e3..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.8.1-librca +java=24-librca diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc deleted file mode 100644 index 17783c7c066b..000000000000 --- a/CODE_OF_CONDUCT.adoc +++ /dev/null @@ -1,44 +0,0 @@ -= Contributor Code of Conduct - -As contributors and maintainers of this project, and in the interest of fostering an open -and welcoming community, we pledge to respect all people who contribute through reporting -issues, posting feature requests, updating documentation, submitting pull requests or -patches, and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, -sexual orientation, disability, personal appearance, body size, race, ethnicity, age, -religion, or nationality. - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, such as physical or electronic addresses, - without explicit permission -* Other unethical or unprofessional conduct - -Project maintainers have the right and responsibility to remove, edit, or reject comments, -commits, code, wiki edits, issues, and other contributions that are not aligned to this -Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors -that they deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, project maintainers commit themselves to fairly and -consistently applying these principles to every aspect of managing this project. Project -maintainers who do not follow or enforce the Code of Conduct may be permanently removed -from the project team. - -This Code of Conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will -be reviewed and investigated and will result in a response that is deemed necessary and -appropriate to the circumstances. Maintainers are obligated to maintain confidentiality -with regard to the reporter of an incident. - -This Code of Conduct is adapted from the -https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at -https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3857859a2e2b..f5b6511d59ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,9 +16,9 @@ First off, thank you for taking the time to contribute! :+1: :tada: ### Code of Conduct -This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc). +This project is governed by the [Spring Code of Conduct](https://github.com/spring-projects/spring-framework#coc-ov-file). By participating you are expected to uphold this code. -Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. +Please report unacceptable behavior to spring-code-of-conduct@spring.io. ### How to Contribute @@ -65,10 +65,6 @@ follow-up reports will need to be created as new issues with a fresh description #### Submit a Pull Request -1. If you have not previously done so, please sign the -[Contributor License Agreement](https://cla.spring.io/sign/spring). You will be reminded -automatically when you submit the PR. - 1. Should you create an issue first? No, just create the pull request and use the description to provide context and motivation, as you would for an issue. If you want to start a discussion first or have already created an issue, once a pull request is @@ -85,8 +81,13 @@ multiple edits or corrections of the same logical change. See [Rewriting History section of Pro Git](https://git-scm.com/book/en/Git-Tools-Rewriting-History) for an overview of streamlining the commit history. +1. All commits must include a _Signed-off-by_ trailer at the end of each commit message +to indicate that the contributor agrees to the Developer Certificate of Origin. +For additional details, please refer to the blog post +[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring). + 1. Format commit messages using 55 characters for the subject line, 72 characters per line -for the description, followed by the issue fixed, e.g. `Closes gh-22276`. See the +for the description, followed by the issue fixed, for example, `Closes gh-22276`. See the [Commit Guidelines section of Pro Git](https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines) for best practices around commit messages, and use `git log` to see some examples. diff --git a/README.md b/README.md index 04dfab87e97c..9dd6a0f15954 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-6.0.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) +# Spring Framework [![Build Status](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=main)](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3Amain) [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring". @@ -6,7 +6,7 @@ Spring provides everything required beyond the Java programming language for cre ## Code of Conduct -This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc). By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. +This project is governed by the [Spring Code of Conduct](https://github.com/spring-projects/spring-framework/?tab=coc-ov-file#contributor-code-of-conduct). By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to spring-code-of-conduct@spring.io. ## Access to Binaries @@ -31,7 +31,7 @@ Information regarding CI builds can be found in the [Spring Framework Concourse ## Stay in Touch -Follow [@SpringCentral](https://twitter.com/springcentral), [@SpringFramework](https://twitter.com/springframework), and its [team members](https://twitter.com/springframework/lists/team/members) on Twitter. In-depth articles can be found at [The Spring Blog](https://spring.io/blog/), and releases are announced via our [releases feed](https://spring.io/blog/category/releases). +Follow [@SpringCentral](https://twitter.com/springcentral), [@SpringFramework](https://twitter.com/springframework), and its [team members](https://twitter.com/springframework/lists/team/members) on 𝕏. In-depth articles can be found at [The Spring Blog](https://spring.io/blog/), and releases are announced via our [releases feed](https://spring.io/blog/category/releases). ## License diff --git a/SECURITY.md b/SECURITY.md index 2a50f06bd5b3..d92c8fa94f42 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,16 +1,10 @@ -# Security Policy +# Reporting a Vulnerability + +Please, [open a draft security advisory](https://github.com/spring-projects/security-advisories/security/advisories/new) if you need to disclose and discuss a security issue in private with the Spring Framework team. Note that we only accept reports against [supported versions](https://spring.io/projects/spring-framework#support). + +For more details, check out our [security policy](https://spring.io/security-policy). ## JAR signing Spring Framework JARs released on Maven Central are signed. You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt - -## Supported Versions - -Please see the -[Spring Framework Versions](https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions) -wiki page. - -## Reporting a Vulnerability - -Please see https://spring.io/security-policy. diff --git a/build.gradle b/build.gradle index 229ee0f91821..b4d4d90de221 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,12 @@ plugins { - id 'io.freefair.aspectj' version '8.4' apply false + id 'io.freefair.aspectj' version '8.13' apply false // kotlinVersion is managed in gradle.properties id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false - id 'org.jetbrains.dokka' version '1.8.20' - id 'org.unbroken-dome.xjc' version '2.0.0' apply false - id 'com.github.ben-manes.versions' version '0.49.0' - id 'com.github.johnrengelman.shadow' version '8.1.1' apply false - id 'de.undercouch.download' version '5.4.0' + id 'org.jetbrains.dokka' version '1.9.20' + id 'com.github.bjornvester.xjc' version '1.8.2' apply false + id 'io.github.goooler.shadow' version '8.1.8' apply false id 'me.champeau.jmh' version '0.7.2' apply false - id 'me.champeau.mrjar' version '0.1.1' + id "net.ltgt.errorprone" version "4.1.0" apply false } ext { @@ -16,23 +14,18 @@ ext { javaProjects = subprojects.findAll { !it.name.startsWith("framework-") } } +description = "Spring Framework" + configure(allprojects) { project -> apply plugin: "org.springframework.build.localdev" 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" } + maven { url = "https://repo.spring.io/milestone" } } if (version.endsWith('-SNAPSHOT')) { - maven { url "https://repo.spring.io/snapshot" } + maven { url = "https://repo.spring.io/snapshot" } } } configurations.all { @@ -61,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 { @@ -77,47 +69,29 @@ 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://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ and weblogic.* packages - "https://www.ibm.com/docs/api/v1/content/SSEQTP_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/", // com.ibm.* - "https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/", // org.jboss.resource.* + "https://jakarta.ee/specifications/platform/11/apidocs/", "https://docs.jboss.org/hibernate/orm/5.6/javadocs/", - "https://eclipse.dev/aspectj/doc/released/aspectj5rt-api", + "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.2.x/current/httpclient5/apidocs/", + "https://hc.apache.org/httpcomponents-client-5.4.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.10.0/api/", + "https://junit.org/junit5/docs/5.12.2/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/" ] as String[] } configure(moduleProjects) { project -> apply from: "${rootDir}/gradle/spring-module.gradle" } - -configure(rootProject) { - description = "Spring Framework" - apply plugin: 'org.springframework.build.api-diff' -} diff --git a/buildSrc/README.md b/buildSrc/README.md index 90dfdd23db84..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,21 +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. -### API Diff +### MultiRelease Jar -This plugin uses the [Gradle JApiCmp](https://github.com/melix/japicmp-gradle-plugin) plugin -to generate API Diff reports for each Spring Framework module. This plugin is applied once on the root -project and creates tasks in each framework module. Unlike previous versions of this part of the build, -there is no need for checking out a specific tag. The plugin will fetch the JARs we want to compare the -current working version with. You can generate the reports for all modules or a single module: +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 +} ``` -./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE -./gradlew :spring-core:apiDiff -PbaselineVersion=5.1.0.RELEASE -``` -The reports are located under `build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/`. - +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 8492a443045a..5bfe6021bf74 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -20,18 +20,20 @@ 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 "me.champeau.gradle:japicmp-gradle-plugin:0.3.0" - implementation "org.gradle:test-retry-gradle-plugin:1.4.1" + 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("org.junit.jupiter:junit-jupiter:${junitJupiterVersion}") } gradlePlugin { plugins { - apiDiffPlugin { - id = "org.springframework.build.api-diff" - implementationClass = "org.springframework.build.api.ApiDiffPlugin" + architecturePlugin { + id = "org.springframework.architecture" + implementationClass = "org.springframework.build.architecture.ArchitecturePlugin" } conventionsPlugin { id = "org.springframework.build.conventions" @@ -41,6 +43,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" @@ -51,3 +57,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 7421d5a6b195..48dbb008b69f 100644 --- a/buildSrc/gradle.properties +++ b/buildSrc/gradle.properties @@ -1,2 +1,4 @@ org.gradle.caching=true -javaFormatVersion=0.0.39 +javaFormatVersion=0.0.42 +junitJupiterVersion=5.11.4 +assertjVersion=3.27.3 \ No newline at end of file diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index bd3e3fae25bb..d93ac4b4143b 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,12 +50,12 @@ 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.12.4"); + checkstyle.setToolVersion("10.23.0"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); - checkstyleDependencies - .add(project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version)); + checkstyleDependencies.add( + project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version)); }); } @@ -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/**"); + 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..99d9d97ef676 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,18 @@ public class JavaConventions { private static final List TEST_COMPILER_ARGS; + /** + * The Java version we should use as the JVM baseline for building the project + */ + 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 +60,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 b3faae547be7..baa741781c4f 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-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,17 +54,16 @@ private void configureTests(Project project, Test test) { 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")); } - test.jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED", + test.jvmArgs( + "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED", - "-Djava.locale.providers=COMPAT"); + "-Xshare:off" + ); } private void configureTestRetryPlugin(Project project, Test test) { diff --git a/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java b/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java deleted file mode 100644 index 9c4258cce2e3..000000000000 --- a/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java +++ /dev/null @@ -1,142 +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.build.api; - -import java.io.File; -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; - -import me.champeau.gradle.japicmp.JapicmpPlugin; -import me.champeau.gradle.japicmp.JapicmpTask; -import org.gradle.api.GradleException; -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.plugins.JavaBasePlugin; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.publish.maven.plugins.MavenPublishPlugin; -import org.gradle.api.tasks.TaskProvider; -import org.gradle.jvm.tasks.Jar; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link Plugin} that applies the {@code "japicmp-gradle-plugin"} - * and create tasks for all subprojects named {@code "spring-*"}, diffing the public API one by one - * and creating the reports in {@code "build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/"}. - *

{@code "./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE"} will output the - * reports for the API diff between the baseline version and the current one for all modules. - * You can limit the report to a single module with - * {@code "./gradlew :spring-core:apiDiff -PbaselineVersion=5.1.0.RELEASE"}. - * - * @author Brian Clozel - */ -public class ApiDiffPlugin implements Plugin { - - private static final Logger logger = LoggerFactory.getLogger(ApiDiffPlugin.class); - - public static final String TASK_NAME = "apiDiff"; - - private static final String BASELINE_VERSION_PROPERTY = "baselineVersion"; - - private static final List PACKAGE_INCLUDES = Collections.singletonList("org.springframework.*"); - - private static final URI SPRING_MILESTONE_REPOSITORY = URI.create("https://repo.spring.io/milestone"); - - @Override - public void apply(Project project) { - if (project.hasProperty(BASELINE_VERSION_PROPERTY) && project.equals(project.getRootProject())) { - project.getPluginManager().apply(JapicmpPlugin.class); - project.getPlugins().withType(JapicmpPlugin.class, - plugin -> applyApiDiffConventions(project)); - } - } - - private void applyApiDiffConventions(Project project) { - String baselineVersion = project.property(BASELINE_VERSION_PROPERTY).toString(); - project.subprojects(subProject -> { - if (subProject.getName().startsWith("spring-")) { - createApiDiffTask(baselineVersion, subProject); - } - }); - } - - private void createApiDiffTask(String baselineVersion, Project project) { - if (isProjectEligible(project)) { - // Add Spring Milestone repository for generating diffs against previous milestones - project.getRootProject() - .getRepositories() - .maven(mavenArtifactRepository -> mavenArtifactRepository.setUrl(SPRING_MILESTONE_REPOSITORY)); - JapicmpTask apiDiff = project.getTasks().create(TASK_NAME, JapicmpTask.class); - apiDiff.setDescription("Generates an API diff report with japicmp"); - apiDiff.setGroup(JavaBasePlugin.DOCUMENTATION_GROUP); - - apiDiff.setOldClasspath(createBaselineConfiguration(baselineVersion, project)); - TaskProvider jar = project.getTasks().withType(Jar.class).named("jar"); - apiDiff.setNewArchives(project.getLayout().files(jar.get().getArchiveFile().get().getAsFile())); - apiDiff.setNewClasspath(getRuntimeClassPath(project)); - apiDiff.setPackageIncludes(PACKAGE_INCLUDES); - apiDiff.setOnlyModified(true); - apiDiff.setIgnoreMissingClasses(true); - // Ignore Kotlin metadata annotations since they contain - // illegal HTML characters and fail the report generation - apiDiff.setAnnotationExcludes(Collections.singletonList("@kotlin.Metadata")); - - apiDiff.setHtmlOutputFile(getOutputFile(baselineVersion, project)); - - apiDiff.dependsOn(project.getTasks().getByName("jar")); - } - } - - private boolean isProjectEligible(Project project) { - return project.getPlugins().hasPlugin(JavaPlugin.class) - && project.getPlugins().hasPlugin(MavenPublishPlugin.class); - } - - private Configuration createBaselineConfiguration(String baselineVersion, Project project) { - String baseline = String.join(":", - project.getGroup().toString(), project.getName(), baselineVersion); - Dependency baselineDependency = project.getDependencies().create(baseline + "@jar"); - Configuration baselineConfiguration = project.getRootProject().getConfigurations().detachedConfiguration(baselineDependency); - try { - // eagerly resolve the baseline configuration to check whether this is a new Spring module - baselineConfiguration.resolve(); - return baselineConfiguration; - } - catch (GradleException exception) { - logger.warn("Could not resolve {} - assuming this is a new Spring module.", baseline); - } - return project.getRootProject().getConfigurations().detachedConfiguration(); - } - - private Configuration getRuntimeClassPath(Project project) { - return project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); - } - - private File getOutputFile(String baseLineVersion, Project project) { - String buildDirectoryPath = project.getRootProject() - .getLayout().getBuildDirectory().getAsFile().get().getAbsolutePath(); - Path outDir = Paths.get(buildDirectoryPath, "reports", "api-diff", - baseLineVersion + "_to_" + project.getRootProject().getVersion()); - return project.file(outDir.resolve(project.getName() + ".html").toString()); - } - -} 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/hint/RuntimeHintsAgentPlugin.java b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java index e0e303812368..fab5ab5134b0 100644 --- a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java @@ -26,7 +26,10 @@ import org.gradle.api.attributes.Usage; import org.gradle.api.attributes.java.TargetJvmVersion; import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.jvm.JvmTestSuite; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.testing.Test; +import org.gradle.testing.base.TestingExtension; import java.util.Collections; @@ -47,17 +50,21 @@ public class RuntimeHintsAgentPlugin implements Plugin { public void apply(Project project) { project.getPlugins().withType(JavaPlugin.class, javaPlugin -> { + TestingExtension testing = project.getExtensions().getByType(TestingExtension.class); + JvmTestSuite jvmTestSuite = (JvmTestSuite) testing.getSuites().getByName("test"); RuntimeHintsAgentExtension agentExtension = createRuntimeHintsAgentExtension(project); - Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> { + TaskProvider agentTest = project.getTasks().register(RUNTIMEHINTS_TEST_TASK, Test.class, test -> { test.useJUnitPlatform(options -> { options.includeTags("RuntimeHintsTests"); }); test.include("**/*Tests.class", "**/*Test.class"); test.systemProperty("java.awt.headless", "true"); test.systemProperty("org.graalvm.nativeimage.imagecode", "runtime"); + test.setTestClassesDirs(jvmTestSuite.getSources().getOutput().getClassesDirs()); + test.setClasspath(jvmTestSuite.getSources().getRuntimeClasspath()); test.getJvmArgumentProviders().add(createRuntimeHintsAgentArgumentProvider(project, agentExtension)); }); - project.getTasks().getByName("check", task -> task.dependsOn(agentTest)); + project.getTasks().named("check", task -> task.dependsOn(agentTest)); project.getDependencies().add(CONFIGURATION_NAME, project.project(":spring-core-test")); }); } 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/ci/README.adoc b/ci/README.adoc deleted file mode 100644 index 9ff0d5b1e86c..000000000000 --- a/ci/README.adoc +++ /dev/null @@ -1,57 +0,0 @@ -== Spring Framework Concourse pipeline - -The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks. -The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline -for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x[Spring Framework 6.0.x]. - -=== Setting up your development environment - -If you're part of the Spring Framework project on GitHub, you can get access to CI management features. -First, you need to go to https://ci.spring.io and install the client CLI for your platform (see bottom right of the screen). - -You can then login with the instance using: - -[source] ----- -$ fly -t spring login -n spring-framework -c https://ci.spring.io ----- - -Once logged in, you should get something like: - -[source] ----- -$ fly ts -name url team expiry -spring https://ci.spring.io spring-framework Wed, 25 Mar 2020 17:45:26 UTC ----- - -=== Pipeline configuration and structure - -The build pipelines are described in `pipeline.yml` file. - -This file is listing Concourse resources, i.e. build inputs and outputs such as container images, artifact repositories, source repositories, notification services, etc. - -It also describes jobs (a job is a sequence of inputs, tasks and outputs); jobs are organized by groups. - -The `pipeline.yml` definition contains `((parameters))` which are loaded from the `parameters.yml` file or from our https://docs.cloudfoundry.org/credhub/[credhub instance]. - -You'll find in this folder the following resources: - -* `pipeline.yml` the build pipeline -* `parameters.yml` the build parameters used for the pipeline -* `images/` holds the container images definitions used in this pipeline -* `scripts/` holds the build scripts that ship within the CI container images -* `tasks` contains the task definitions used in the main `pipeline.yml` - -=== Updating the build pipeline - -Updating files on the repository is not enough to update the build pipeline, as changes need to be applied. - -The pipeline can be deployed using the following command: - -[source] ----- -$ fly -t spring set-pipeline -p spring-framework-6.0.x -c ci/pipeline.yml -l ci/parameters.yml ----- - -NOTE: This assumes that you have credhub integration configured with the appropriate secrets. diff --git a/ci/config/changelog-generator.yml b/ci/config/changelog-generator.yml deleted file mode 100644 index 2252d20802e4..000000000000 --- a/ci/config/changelog-generator.yml +++ /dev/null @@ -1,20 +0,0 @@ -changelog: - repository: spring-projects/spring-framework - sections: - - title: ":star: New Features" - labels: - - "type: enhancement" - - title: ":lady_beetle: Bug Fixes" - labels: - - "type: bug" - - "type: regression" - - title: ":notebook_with_decorative_cover: Documentation" - labels: - - "type: documentation" - - title: ":hammer: Dependency Upgrades" - sort: "title" - labels: - - "type: dependency-upgrade" - contributors: - exclude: - names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll", "simonbasle"] diff --git a/ci/config/release-scripts.yml b/ci/config/release-scripts.yml deleted file mode 100644 index d31f8cba00dc..000000000000 --- a/ci/config/release-scripts.yml +++ /dev/null @@ -1,10 +0,0 @@ -logging: - level: - io.spring.concourse: DEBUG -spring: - main: - banner-mode: off -sonatype: - exclude: - - 'build-info\.json' - - '.*\.zip' diff --git a/ci/images/README.adoc b/ci/images/README.adoc deleted file mode 100644 index 6da9addd9ca5..000000000000 --- a/ci/images/README.adoc +++ /dev/null @@ -1,21 +0,0 @@ -== CI Images - -These images are used by CI to run the actual builds. - -To build the image locally run the following from this directory: - ----- -$ docker build --no-cache -f /Dockerfile . ----- - -For example - ----- -$ docker build --no-cache -f spring-framework-ci-image/Dockerfile . ----- - -To test run: - ----- -$ docker run -it --entrypoint /bin/bash ----- diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile deleted file mode 100644 index 3d2a1e392206..000000000000 --- a/ci/images/ci-image/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM ubuntu:jammy-20231004 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh - -ENV JAVA_HOME /opt/openjdk/java17 -ENV JDK17 /opt/openjdk/java17 -ENV JDK21 /opt/openjdk/java21 -ENV JDK22 /opt/openjdk/java22 - -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh deleted file mode 100755 index cdecc5f65bf8..000000000000 --- a/ci/images/get-jdk-url.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e - -case "$1" in - java17) - echo "https://download.bell-sw.com/java/17.0.9+11/bellsoft-jdk17.0.9+11-linux-amd64.tar.gz" - ;; - java21) - echo "https://download.bell-sw.com/java/21.0.1+12/bellsoft-jdk21.0.1+12-linux-amd64.tar.gz" - ;; - java22) - echo "https://download.java.net/java/early_access/jdk22/19/GPL/openjdk-22-ea+19_linux-x64_bin.tar.gz" - ;; - *) - echo $"Unknown java version" - exit 1 -esac diff --git a/ci/images/setup.sh b/ci/images/setup.sh deleted file mode 100755 index 77207dae4967..000000000000 --- a/ci/images/setup.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -set -ex - -########################################################### -# UTILS -########################################################### - -export DEBIAN_FRONTEND=noninteractive -apt-get update -apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig -ln -fs /usr/share/zoneinfo/UTC /etc/localtime -dpkg-reconfigure --frontend noninteractive tzdata -rm -rf /var/lib/apt/lists/* - -curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh - -########################################################### -# JAVA -########################################################### - -mkdir -p /opt/openjdk -pushd /opt/openjdk > /dev/null -for jdk in java17 java21 java22 -do - JDK_URL=$( /get-jdk-url.sh $jdk ) - mkdir $jdk - pushd $jdk > /dev/null - curl -L ${JDK_URL} | tar zx --strip-components=1 - test -f bin/java - test -f bin/javac - popd > /dev/null -done -popd - -########################################################### -# GRADLE ENTERPRISE -########################################################### -cd / -mkdir ~/.gradle -echo 'systemProp.user.name=concourse' > ~/.gradle/gradle.properties diff --git a/ci/parameters.yml b/ci/parameters.yml deleted file mode 100644 index 842926e0eb00..000000000000 --- a/ci/parameters.yml +++ /dev/null @@ -1,11 +0,0 @@ -github-repo: "https://github.com/spring-projects/spring-framework.git" -github-repo-name: "spring-projects/spring-framework" -sonatype-staging-profile: "org.springframework" -docker-hub-organization: "springci" -artifactory-server: "https://repo.spring.io" -branch: "main" -milestone: "6.1.x" -build-name: "spring-framework" -pipeline-name: "spring-framework" -concourse-url: "https://ci.spring.io" -task-timeout: 1h00m diff --git a/ci/pipeline.yml b/ci/pipeline.yml deleted file mode 100644 index 538d62555778..000000000000 --- a/ci/pipeline.yml +++ /dev/null @@ -1,433 +0,0 @@ -anchors: - git-repo-resource-source: &git-repo-resource-source - uri: ((github-repo)) - username: ((github-username)) - password: ((github-ci-release-token)) - branch: ((branch)) - gradle-enterprise-task-params: &gradle-enterprise-task-params - GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) - GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username)) - GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password)) - sonatype-task-params: &sonatype-task-params - SONATYPE_USERNAME: ((sonatype-username)) - SONATYPE_PASSWORD: ((sonatype-password)) - SONATYPE_URL: ((sonatype-url)) - SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile)) - artifactory-task-params: &artifactory-task-params - ARTIFACTORY_SERVER: ((artifactory-server)) - ARTIFACTORY_USERNAME: ((artifactory-username)) - ARTIFACTORY_PASSWORD: ((artifactory-password)) - build-project-task-params: &build-project-task-params - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params - docker-resource-source: &docker-resource-source - username: ((docker-hub-username)) - password: ((docker-hub-password)) - slack-fail-params: &slack-fail-params - text: > - :concourse-failed: - [$TEXT_FILE_CONTENT] - text_file: git-repo/build/build-scan-uri.txt - silent: true - icon_emoji: ":concourse:" - username: concourse-ci - changelog-task-params: &changelog-task-params - name: generated-changelog/tag - tag: generated-changelog/tag - body: generated-changelog/changelog.md - github-task-params: &github-task-params - GITHUB_USERNAME: ((github-username)) - GITHUB_TOKEN: ((github-ci-release-token)) - -resource_types: -- name: registry-image - type: registry-image - source: - <<: *docker-resource-source - repository: concourse/registry-image-resource - tag: 1.8.0 -- name: artifactory-resource - type: registry-image - source: - <<: *docker-resource-source - repository: springio/artifactory-resource - tag: 0.0.18 -- name: github-release - type: registry-image - source: - <<: *docker-resource-source - repository: concourse/github-release-resource - tag: 1.8.0 -- name: github-status-resource - type: registry-image - source: - <<: *docker-resource-source - repository: dpb587/github-status-resource - tag: master -- name: slack-notification - type: registry-image - source: - <<: *docker-resource-source - repository: cfcommunity/slack-notification-resource - tag: latest -resources: -- name: git-repo - type: git - icon: github - source: - <<: *git-repo-resource-source -- name: ci-images-git-repo - type: git - icon: github - source: - uri: ((github-repo)) - branch: ((branch)) - paths: ["ci/images/*"] -- name: ci-image - type: registry-image - icon: docker - source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-ci - tag: ((milestone)) -- name: every-morning - type: time - icon: alarm - source: - start: 8:00 AM - stop: 9:00 AM - location: Europe/Vienna -- name: artifactory-repo - type: artifactory-resource - icon: package-variant - source: - uri: ((artifactory-server)) - username: ((artifactory-username)) - password: ((artifactory-password)) - build_name: ((build-name)) -- name: repo-status-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: build -- name: repo-status-jdk21-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: jdk21-build -- name: repo-status-jdk22-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: jdk22-build -- name: slack-alert - type: slack-notification - icon: slack - source: - url: ((slack-webhook-url)) -- name: github-pre-release - type: github-release - icon: briefcase-download-outline - source: - owner: spring-projects - repository: spring-framework - access_token: ((github-ci-release-token)) - pre_release: true - release: false -- name: github-release - type: github-release - icon: briefcase-download - source: - owner: spring-projects - repository: spring-framework - access_token: ((github-ci-release-token)) - pre_release: false -jobs: -- name: build-ci-images - plan: - - get: git-repo - - get: ci-images-git-repo - trigger: true - - task: build-ci-image - privileged: true - file: git-repo/ci/tasks/build-ci-image.yml - output_mapping: - image: ci-image - vars: - ci-image-name: ci-image - <<: *docker-resource-source - - put: ci-image - params: - image: ci-image/image.tar -- name: build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - trigger: true - - put: repo-status-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: build-project - image: ci-image - file: git-repo/ci/tasks/build-project.yml - privileged: true - timeout: ((task-timeout)) - params: - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-build - params: { state: "success", commit: "git-repo" } - - put: artifactory-repo - params: &artifactory-params - signing_key: ((signing-key)) - signing_passphrase: ((signing-passphrase)) - repo: libs-snapshot-local - folder: distribution-repository - build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}" - build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}" - disable_checksum_uploads: true - threads: 8 - artifact_set: - - include: - - "/**/framework-api-*.zip" - properties: - "zip.name": "spring-framework" - "zip.displayname": "Spring Framework" - "zip.deployed": "false" - - include: - - "/**/framework-api-*-docs.zip" - properties: - "zip.type": "docs" - - include: - - "/**/framework-api-*-schema.zip" - properties: - "zip.type": "schema" - get_params: - threads: 8 -- name: jdk21-build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - - get: every-morning - trigger: true - - put: repo-status-jdk21-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: check-project - image: ci-image - file: git-repo/ci/tasks/check-project.yml - privileged: true - timeout: ((task-timeout)) - params: - TEST_TOOLCHAIN: 21 - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-jdk21-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-jdk21-build - params: { state: "success", commit: "git-repo" } -- name: jdk22-build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - - put: repo-status-jdk22-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: check-project - image: ci-image - file: git-repo/ci/tasks/check-project.yml - privileged: true - timeout: ((task-timeout)) - params: - TEST_TOOLCHAIN: 22 - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-jdk22-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-jdk22-build - params: { state: "success", commit: "git-repo" } -- name: stage-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: M - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-milestone] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: M - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: M - <<: *github-task-params - <<: *docker-resource-source - - put: github-pre-release - params: - <<: *changelog-task-params -- name: stage-rc - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: RC - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-rc - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-rc] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: RC - <<: *docker-resource-source - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RC - <<: *github-task-params - - put: github-pre-release - params: - <<: *changelog-task-params -- name: stage-release - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: RELEASE - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-release - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-release] - params: - download_artifacts: true - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: RELEASE - <<: *docker-resource-source - <<: *artifactory-task-params - <<: *sonatype-task-params -- name: create-github-release - serial: true - plan: - - get: ci-image - - get: git-repo - - get: artifactory-repo - trigger: true - passed: [promote-release] - params: - download_artifacts: false - save_build_info: true - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RELEASE - <<: *docker-resource-source - <<: *github-task-params - - put: github-release - params: - <<: *changelog-task-params - -groups: -- name: "builds" - jobs: ["build", "jdk21-build", "jdk22-build"] -- name: "releases" - jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"] -- name: "ci-images" - jobs: ["build-ci-images"] diff --git a/ci/scripts/build-pr.sh b/ci/scripts/build-pr.sh deleted file mode 100755 index 0c38429450eb..000000000000 --- a/ci/scripts/build-pr.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \ - --no-daemon --max-workers=4 check -popd > /dev/null diff --git a/ci/scripts/build-project.sh b/ci/scripts/build-project.sh deleted file mode 100755 index ee784893c41b..000000000000 --- a/ci/scripts/build-project.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK21,JDK22 \ - --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository -popd > /dev/null diff --git a/ci/scripts/check-project.sh b/ci/scripts/check-project.sh deleted file mode 100755 index 3e6e1069ae07..000000000000 --- a/ci/scripts/check-project.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \ - -PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check antora -popd > /dev/null diff --git a/ci/scripts/common.sh b/ci/scripts/common.sh deleted file mode 100644 index 1accaa616732..000000000000 --- a/ci/scripts/common.sh +++ /dev/null @@ -1,2 +0,0 @@ -source /opt/concourse-java.sh -setup_symlinks \ No newline at end of file diff --git a/ci/scripts/generate-changelog.sh b/ci/scripts/generate-changelog.sh deleted file mode 100755 index d3d2b97e5dba..000000000000 --- a/ci/scripts/generate-changelog.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e - -CONFIG_DIR=git-repo/ci/config -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) - -java -jar /github-changelog-generator.jar \ - --spring.config.location=${CONFIG_DIR}/changelog-generator.yml \ - ${version} generated-changelog/changelog.md - -echo ${version} > generated-changelog/version -echo v${version} > generated-changelog/tag diff --git a/ci/scripts/promote-version.sh b/ci/scripts/promote-version.sh deleted file mode 100755 index bd1600191a79..000000000000 --- a/ci/scripts/promote-version.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -CONFIG_DIR=git-repo/ci/config - -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) -export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json - -java -jar /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; } - -java -jar /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } - -echo "Promotion complete" -echo $version > version/version diff --git a/ci/scripts/stage-version.sh b/ci/scripts/stage-version.sh deleted file mode 100755 index 7cf2e3b3660f..000000000000 --- a/ci/scripts/stage-version.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -git fetch --tags --all > /dev/null -popd > /dev/null - -git clone git-repo stage-git-repo > /dev/null - -pushd stage-git-repo > /dev/null - -snapshotVersion=$( awk -F '=' '$1 == "version" { print $2 }' gradle.properties ) -if [[ $RELEASE_TYPE = "M" ]]; then - stageVersion=$( get_next_milestone_release $snapshotVersion) - nextVersion=$snapshotVersion -elif [[ $RELEASE_TYPE = "RC" ]]; then - stageVersion=$( get_next_rc_release $snapshotVersion) - nextVersion=$snapshotVersion -elif [[ $RELEASE_TYPE = "RELEASE" ]]; then - stageVersion=$( get_next_release $snapshotVersion) - nextVersion=$( bump_version_number $snapshotVersion) -else - echo "Unknown release type $RELEASE_TYPE" >&2; exit 1; -fi - -echo "Staging $stageVersion (next version will be $nextVersion)" -sed -i "s/version=$snapshotVersion/version=$stageVersion/" gradle.properties - -git config user.name "Spring Builds" > /dev/null -git config user.email "spring-builds@users.noreply.github.com" > /dev/null -git add gradle.properties > /dev/null -git commit -m"Release v$stageVersion" > /dev/null -git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null - -./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \ - build publishAllPublicationsToDeploymentRepository - -git reset --hard HEAD^ > /dev/null -if [[ $nextVersion != $snapshotVersion ]]; then - echo "Setting next development version (v$nextVersion)" - sed -i "s/version=$snapshotVersion/version=$nextVersion/" gradle.properties - git add gradle.properties > /dev/null - git commit -m"Next development version (v$nextVersion)" > /dev/null -fi; - -echo "Staging Complete" - -popd > /dev/null diff --git a/ci/tasks/build-ci-image.yml b/ci/tasks/build-ci-image.yml deleted file mode 100644 index 28afb97cb629..000000000000 --- a/ci/tasks/build-ci-image.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -platform: linux -image_resource: - type: registry-image - source: - repository: concourse/oci-build-task - tag: 0.10.0 - username: ((docker-hub-username)) - password: ((docker-hub-password)) -inputs: - - name: ci-images-git-repo -outputs: - - name: image -caches: - - path: ci-image-cache -params: - CONTEXT: ci-images-git-repo/ci/images - DOCKERFILE: ci-images-git-repo/ci/images/ci-image/Dockerfile - DOCKER_HUB_AUTH: ((docker-hub-auth)) -run: - path: /bin/sh - args: - - "-c" - - | - mkdir -p /root/.docker - cat > /root/.docker/config.json < def Properties schemas = new Properties(); diff --git a/framework-docs/antora-playbook.yml b/framework-docs/antora-playbook.yml new file mode 100644 index 000000000000..62f0f71a8a3f --- /dev/null +++ b/framework-docs/antora-playbook.yml @@ -0,0 +1,39 @@ +antora: + extensions: + - require: '@springio/antora-extensions' + root_component_name: 'framework' +site: + title: Spring Framework + url: https://docs.spring.io/spring-framework/reference + robots: allow +git: + ensure_git_suffix: false +content: + sources: + - url: https://github.com/spring-projects/spring-framework + # Refname matching: + # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ + branches: ['main', '{6..9}.+({1..9}).x'] + tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})'] + start_path: framework-docs +asciidoc: + extensions: + - '@asciidoctor/tabs' + - '@springio/asciidoctor-extensions' + - '@springio/asciidoctor-extensions/include-code-extension' + attributes: + page-stackoverflow-url: https://stackoverflow.com/questions/tagged/spring + page-pagination: '' + hide-uri-scheme: '@' + tabs-sync-option: '@' + include-java: 'example$docs-src/main/java/org/springframework/docs' +urls: + latest_version_segment_strategy: redirect:to + latest_version_segment: '' + redirect_facility: httpd +runtime: + log: + failure_level: warn +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip diff --git a/framework-docs/antora.yml b/framework-docs/antora.yml index 642ecd19f6cd..87f730b85743 100644 --- a/framework-docs/antora.yml +++ b/framework-docs/antora.yml @@ -17,16 +17,81 @@ asciidoc: # FIXME: the copyright is not removed # FIXME: The package is not renamed chomp: 'all' + fold: 'all' + table-stripes: 'odd' include-java: 'example$docs-src/main/java/org/springframework/docs' - spring-framework-main-code: 'https://github.com/spring-projects/spring-framework/tree/main' + include-kotlin: 'example$docs-src/main/kotlin/org/springframework/docs' + include-xml: 'example$docs-src/main/resources/org/springframework/docs' + spring-site: 'https://spring.io' + spring-site-blog: '{spring-site}/blog' + spring-site-cve: "{spring-site}/security" + spring-site-guides: '{spring-site}/guides' + spring-site-projects: '{spring-site}/projects' + spring-site-tools: "{spring-site}/tools" + spring-org: 'spring-projects' + spring-github-org: "https://github.com/{spring-org}" + spring-framework-github: "https://github.com/{spring-org}/spring-framework" + spring-framework-code: '{spring-framework-github}/tree/main' + spring-framework-issues: '{spring-framework-github}/issues' + spring-framework-wiki: '{spring-framework-github}/wiki' + # Docs docs-site: 'https://docs.spring.io' - docs-spring: "{docs-site}/spring-framework/docs/{spring-version}" - docs-spring-framework: '{docs-site}/spring-framework/docs/{spring-version}' - api-spring-framework: '{docs-spring-framework}/javadoc-api/org/springframework' - docs-graalvm: 'https://www.graalvm.org/22.3/reference-manual' - docs-spring-boot: '{docs-site}/spring-boot/docs/current/reference' + spring-framework-docs-root: '{docs-site}/spring-framework/docs' + spring-framework-api: '{spring-framework-docs-root}/{spring-version}/javadoc-api/org/springframework' + spring-framework-api-kdoc: '{spring-framework-docs-root}/{spring-version}/kdoc-api' + spring-framework-reference: '{spring-framework-docs-root}/{spring-version}/reference' + # + # Other Spring portfolio projects + spring-boot-docs: '{docs-site}/spring-boot' + spring-boot-docs-ref: '{spring-boot-docs}/reference' + spring-boot-issues: '{spring-github-org}/spring-boot/issues' + # TODO add more projects / links or just build up on {docs-site}? + # TODO rename the below using new conventions docs-spring-gemfire: '{docs-site}/spring-gemfire/docs/current/reference' docs-spring-security: '{docs-site}/spring-security/reference' - gh-rsocket: 'https://github.com/rsocket' - gh-rsocket-extensions: '{gh-rsocket}/rsocket/blob/master/Extensions' - gh-rsocket-java: '{gh-rsocket}/rsocket-java{gh-rsocket}/rsocket-java' \ No newline at end of file + docs-spring-session: '{docs-site}/spring-session/reference' + # + # External projects URLs and related attributes + aspectj-site: 'https://www.eclipse.org/aspectj' + aspectj-docs: "{aspectj-site}/doc/released" + aspectj-api: "{aspectj-docs}/runtime-api" + aspectj-docs-devguide: "{aspectj-docs}/devguide" + aspectj-docs-progguide: "{aspectj-docs}/progguide" + assertj-docs: 'https://assertj.github.io/doc' + baeldung-blog: 'https://www.baeldung.com' + bean-validation-site: 'https://beanvalidation.org' + graalvm-docs: 'https://www.graalvm.org/22.3/reference-manual' + hibernate-validator-site: 'https://hibernate.org/validator/' + jackson-docs: 'https://fasterxml.github.io' + jackson-github-org: 'https://github.com/FasterXML' + java-api: 'https://docs.oracle.com/en/java/javase/17/docs/api' + java-tutorial: 'https://docs.oracle.com/javase/tutorial' + JSR: 'https://www.jcp.org/en/jsr/detail?id=' + kotlin-site: 'https://kotlinlang.org' + kotlin-docs: '{kotlin-site}/docs' + kotlin-api: '{kotlin-site}/api/latest' + kotlin-coroutines-api: '{kotlin-site}/api/kotlinx.coroutines' + kotlin-github-org: 'https://github.com/Kotlin' + kotlin-issues: 'https://youtrack.jetbrains.com/issue' + micrometer-docs: 'https://docs.micrometer.io/micrometer/reference' + micrometer-context-propagation-docs: 'https://docs.micrometer.io/context-propagation/reference' + petclinic-github-org: 'https://github.com/spring-petclinic' + reactive-streams-site: 'https://www.reactive-streams.org' + reactive-streams-spec: 'https://github.com/reactive-streams/reactive-streams-jvm/blob/master/README.md#specification' + reactor-github-org: 'https://github.com/reactor' + reactor-site: 'https://projectreactor.io' + rsocket-github-org: 'https://github.com/rsocket' + rsocket-java: '{rsocket-github-org}/rsocket-java' + rsocket-java-code: '{rsocket-java}/tree/master/' + rsocket-protocol-extensions: '{rsocket-github-org}/rsocket/tree/master/Extensions' + rsocket-site: 'https://rsocket.io' + rfc-site: 'https://datatracker.ietf.org/doc/html' + sockjs-client: 'https://github.com/sockjs/sockjs-client' + sockjs-protocol: 'https://github.com/sockjs/sockjs-protocol' + sockjs-protocol-site: "https://sockjs.github.io/sockjs-protocol" + stackoverflow-site: 'https://stackoverflow.com' + stackoverflow-questions: '{stackoverflow-site}/questions' + stackoverflow-spring-tag: "{stackoverflow-questions}/tagged/spring" + stackoverflow-spring-kotlin-tags: "{stackoverflow-spring-tag}+kotlin" + testcontainers-site: 'https://www.testcontainers.org' + vavr-docs: 'https://vavr-io.github.io/vavr-docs' \ No newline at end of file diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index c8e423204567..4f28c8a8a4e8 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' @@ -6,30 +8,14 @@ plugins { description = "Spring Framework Docs" +apply from: "${rootDir}/gradle/ide.gradle" apply from: "${rootDir}/gradle/publications.gradle" antora { - version = '3.2.0-alpha.2' - playbook = 'cached-antora-playbook.yml' - playbookProvider { - repository = 'spring-projects/spring-framework' - branch = 'docs-build' - path = 'lib/antora/templates/per-branch-antora-playbook.yml' - checkLocalBranch = true - } - options = ['--clean', '--stacktrace'] + options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] environment = [ - 'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691', - 'ALGOLIA_APP_ID': '244V8V9FGG', - 'ALGOLIA_INDEX_NAME': 'framework-docs' - ] - dependencies = [ - '@antora/atlas-extension': '1.0.0-alpha.1', - '@antora/collector-extension': '1.0.0-alpha.3', - '@asciidoctor/tabs': '1.0.0-beta.3', - '@opendevise/antora-release-line-extension': '1.0.0-alpha.2', - '@springio/antora-extensions': '1.3.0', - '@springio/asciidoctor-extensions': '1.0.0-alpha.9' + 'BUILD_REFNAME': 'HEAD', + 'BUILD_VERSION': project.version, ] } @@ -53,17 +39,44 @@ javadoc { repositories { maven { - url "https://repo.spring.io/release" + url = "https://repo.spring.io/release" } } -dependencies { - api(project(":spring-context")) - api(project(":spring-jms")) - api(project(":spring-web")) - api("jakarta.jms:jakarta.jms-api") - api("jakarta.servlet:jakarta.servlet-api") +// 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.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 37cb971836c2..7df6fde4d12f 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[] @@ -39,8 +40,8 @@ ** xref:core/resources.adoc[] ** xref:core/validation.adoc[] *** xref:core/validation/validator.adoc[] -*** xref:core/validation/conversion.adoc[] *** xref:core/validation/beans-beans.adoc[] +*** xref:core/validation/conversion.adoc[] *** xref:core/validation/convert.adoc[] *** xref:core/validation/format.adoc[] *** xref:core/validation/format-configuring-formatting-globaldatetimeformat.adoc[] @@ -60,6 +61,7 @@ **** xref:core/expressions/language-ref/constructors.adoc[] **** xref:core/expressions/language-ref/variables.adoc[] **** xref:core/expressions/language-ref/functions.adoc[] +**** xref:core/expressions/language-ref/varargs.adoc[] **** xref:core/expressions/language-ref/bean-references.adoc[] **** xref:core/expressions/language-ref/operator-ternary.adoc[] **** xref:core/expressions/language-ref/operator-elvis.adoc[] @@ -100,92 +102,11 @@ *** 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[] *** xref:core/appendix/xml-custom.adoc[] *** xref:core/appendix/application-startup-steps.adoc[] -* xref:testing.adoc[] -** xref:testing/introduction.adoc[] -** xref:testing/unit.adoc[] -** xref:testing/integration.adoc[] -** xref:testing/support-jdbc.adoc[] -** xref:testing/testcontext-framework.adoc[] -*** xref:testing/testcontext-framework/key-abstractions.adoc[] -*** xref:testing/testcontext-framework/bootstrapping.adoc[] -*** xref:testing/testcontext-framework/tel-config.adoc[] -*** xref:testing/testcontext-framework/application-events.adoc[] -*** xref:testing/testcontext-framework/test-execution-events.adoc[] -*** xref:testing/testcontext-framework/ctx-management.adoc[] -**** xref:testing/testcontext-framework/ctx-management/xml.adoc[] -**** xref:testing/testcontext-framework/ctx-management/groovy.adoc[] -**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[] -**** xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[] -**** xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[] -**** xref:testing/testcontext-framework/ctx-management/initializers.adoc[] -**** xref:testing/testcontext-framework/ctx-management/inheritance.adoc[] -**** xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[] -**** xref:testing/testcontext-framework/ctx-management/property-sources.adoc[] -**** xref:testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc[] -**** xref:testing/testcontext-framework/ctx-management/web.adoc[] -**** xref:testing/testcontext-framework/ctx-management/web-mocks.adoc[] -**** xref:testing/testcontext-framework/ctx-management/caching.adoc[] -**** xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[] -**** xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[] -*** xref:testing/testcontext-framework/fixture-di.adoc[] -*** xref:testing/testcontext-framework/web-scoped-beans.adoc[] -*** xref:testing/testcontext-framework/tx.adoc[] -*** xref:testing/testcontext-framework/executing-sql.adoc[] -*** xref:testing/testcontext-framework/parallel-test-execution.adoc[] -*** xref:testing/testcontext-framework/support-classes.adoc[] -*** xref:testing/testcontext-framework/aot.adoc[] -** xref:testing/webtestclient.adoc[] -** xref:testing/spring-mvc-test-framework.adoc[] -*** xref:testing/spring-mvc-test-framework/server.adoc[] -*** xref:testing/spring-mvc-test-framework/server-static-imports.adoc[] -*** xref:testing/spring-mvc-test-framework/server-setup-options.adoc[] -*** xref:testing/spring-mvc-test-framework/server-setup-steps.adoc[] -*** xref:testing/spring-mvc-test-framework/server-performing-requests.adoc[] -*** xref:testing/spring-mvc-test-framework/server-defining-expectations.adoc[] -*** xref:testing/spring-mvc-test-framework/async-requests.adoc[] -*** xref:testing/spring-mvc-test-framework/vs-streaming-response.adoc[] -*** xref:testing/spring-mvc-test-framework/server-filters.adoc[] -*** xref:testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc[] -*** xref:testing/spring-mvc-test-framework/server-resources.adoc[] -*** xref:testing/spring-mvc-test-framework/server-htmlunit.adoc[] -**** xref:testing/spring-mvc-test-framework/server-htmlunit/why.adoc[] -**** xref:testing/spring-mvc-test-framework/server-htmlunit/mah.adoc[] -**** xref:testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc[] -**** xref:testing/spring-mvc-test-framework/server-htmlunit/geb.adoc[] -** xref:testing/spring-mvc-test-client.adoc[] -** xref:testing/appendix.adoc[] -*** xref:testing/annotations.adoc[] -**** xref:testing/annotations/integration-standard.adoc[] -**** xref:testing/annotations/integration-spring.adoc[] -***** xref:testing/annotations/integration-spring/annotation-bootstrapwith.adoc[] -***** xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[] -***** xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[] -***** xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[] -***** xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[] -***** xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[] -***** xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[] -***** xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[] -***** xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[] -***** xref:testing/annotations/integration-spring/annotation-testexecutionlisteners.adoc[] -***** xref:testing/annotations/integration-spring/annotation-recordapplicationevents.adoc[] -***** xref:testing/annotations/integration-spring/annotation-commit.adoc[] -***** xref:testing/annotations/integration-spring/annotation-rollback.adoc[] -***** xref:testing/annotations/integration-spring/annotation-beforetransaction.adoc[] -***** xref:testing/annotations/integration-spring/annotation-aftertransaction.adoc[] -***** xref:testing/annotations/integration-spring/annotation-sql.adoc[] -***** xref:testing/annotations/integration-spring/annotation-sqlconfig.adoc[] -***** xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[] -***** xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[] -**** xref:testing/annotations/integration-junit4.adoc[] -**** xref:testing/annotations/integration-junit-jupiter.adoc[] -**** xref:testing/annotations/integration-meta.adoc[] -*** xref:testing/resources.adoc[] * xref:data-access.adoc[] ** xref:data-access/transaction.adoc[] *** xref:data-access/transaction/motivation.adoc[] @@ -240,10 +161,10 @@ **** 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[] +*** xref:web/webmvc/message-converters.adoc[] *** xref:web/webmvc/mvc-controller.adoc[] **** xref:web/webmvc/mvc-controller/ann.adoc[] **** xref:web/webmvc/mvc-controller/ann-requestmapping.adoc[] @@ -284,6 +205,7 @@ **** xref:web/webmvc-view/mvc-freemarker.adoc[] **** xref:web/webmvc-view/mvc-groovymarkup.adoc[] **** xref:web/webmvc-view/mvc-script.adoc[] +**** xref:web/webmvc-view/mvc-fragments.adoc[] **** xref:web/webmvc-view/mvc-jsp.adoc[] **** xref:web/webmvc-view/mvc-feeds.adoc[] **** xref:web/webmvc-view/mvc-document.adoc[] @@ -391,6 +313,97 @@ ** xref:web/webflux-test.adoc[] ** xref:rsocket.adoc[] ** xref:web/webflux-reactive-libraries.adoc[] +* xref:testing.adoc[] +** xref:testing/introduction.adoc[] +** xref:testing/unit.adoc[] +** xref:testing/integration.adoc[] +** xref:testing/support-jdbc.adoc[] +** xref:testing/testcontext-framework.adoc[] +*** xref:testing/testcontext-framework/key-abstractions.adoc[] +*** xref:testing/testcontext-framework/bootstrapping.adoc[] +*** xref:testing/testcontext-framework/tel-config.adoc[] +*** xref:testing/testcontext-framework/application-events.adoc[] +*** xref:testing/testcontext-framework/test-execution-events.adoc[] +*** xref:testing/testcontext-framework/ctx-management.adoc[] +**** xref:testing/testcontext-framework/ctx-management/xml.adoc[] +**** xref:testing/testcontext-framework/ctx-management/groovy.adoc[] +**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[] +**** xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[] +**** xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[] +**** xref:testing/testcontext-framework/ctx-management/initializers.adoc[] +**** xref:testing/testcontext-framework/ctx-management/inheritance.adoc[] +**** xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[] +**** xref:testing/testcontext-framework/ctx-management/property-sources.adoc[] +**** xref:testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc[] +**** xref:testing/testcontext-framework/ctx-management/web.adoc[] +**** xref:testing/testcontext-framework/ctx-management/web-mocks.adoc[] +**** xref:testing/testcontext-framework/ctx-management/caching.adoc[] +**** xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[] +**** xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[] +*** xref:testing/testcontext-framework/fixture-di.adoc[] +*** xref:testing/testcontext-framework/bean-overriding.adoc[] +*** xref:testing/testcontext-framework/web-scoped-beans.adoc[] +*** xref:testing/testcontext-framework/tx.adoc[] +*** xref:testing/testcontext-framework/executing-sql.adoc[] +*** xref:testing/testcontext-framework/parallel-test-execution.adoc[] +*** xref:testing/testcontext-framework/support-classes.adoc[] +*** xref:testing/testcontext-framework/aot.adoc[] +** xref:testing/webtestclient.adoc[] +** xref:testing/mockmvc.adoc[] +*** xref:testing/mockmvc/overview.adoc[] +*** xref:testing/mockmvc/setup-options.adoc[] +*** xref:testing/mockmvc/hamcrest.adoc[] +**** xref:testing/mockmvc/hamcrest/static-imports.adoc[] +**** xref:testing/mockmvc/hamcrest/setup.adoc[] +**** xref:testing/mockmvc/hamcrest/setup-steps.adoc[] +**** xref:testing/mockmvc/hamcrest/requests.adoc[] +**** xref:testing/mockmvc/hamcrest/expectations.adoc[] +**** xref:testing/mockmvc/hamcrest/async-requests.adoc[] +**** xref:testing/mockmvc/hamcrest/vs-streaming-response.adoc[] +**** xref:testing/mockmvc/hamcrest/filters.adoc[] +*** xref:testing/mockmvc/assertj.adoc[] +**** xref:testing/mockmvc/assertj/setup.adoc[] +**** xref:testing/mockmvc/assertj/requests.adoc[] +**** xref:testing/mockmvc/assertj/assertions.adoc[] +**** xref:testing/mockmvc/assertj/integration.adoc[] +*** xref:testing/mockmvc/htmlunit.adoc[] +**** xref:testing/mockmvc/htmlunit/why.adoc[] +**** xref:testing/mockmvc/htmlunit/mah.adoc[] +**** xref:testing/mockmvc/htmlunit/webdriver.adoc[] +**** xref:testing/mockmvc/htmlunit/geb.adoc[] +*** xref:testing/mockmvc/vs-end-to-end-integration-tests.adoc[] +*** xref:testing/mockmvc/resources.adoc[] +** xref:testing/spring-mvc-test-client.adoc[] +** xref:testing/appendix.adoc[] +*** xref:testing/annotations.adoc[] +**** xref:testing/annotations/integration-standard.adoc[] +**** xref:testing/annotations/integration-spring.adoc[] +***** xref:testing/annotations/integration-spring/annotation-bootstrapwith.adoc[] +***** xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[] +***** xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[] +***** xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[] +***** xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[] +***** xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[] +***** xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[] +***** xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[] +***** xref:testing/annotations/integration-spring/annotation-testbean.adoc[] +***** xref:testing/annotations/integration-spring/annotation-mockitobean.adoc[] +***** xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[] +***** xref:testing/annotations/integration-spring/annotation-testexecutionlisteners.adoc[] +***** xref:testing/annotations/integration-spring/annotation-recordapplicationevents.adoc[] +***** xref:testing/annotations/integration-spring/annotation-commit.adoc[] +***** xref:testing/annotations/integration-spring/annotation-rollback.adoc[] +***** xref:testing/annotations/integration-spring/annotation-beforetransaction.adoc[] +***** xref:testing/annotations/integration-spring/annotation-aftertransaction.adoc[] +***** xref:testing/annotations/integration-spring/annotation-sql.adoc[] +***** xref:testing/annotations/integration-spring/annotation-sqlconfig.adoc[] +***** xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[] +***** xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[] +***** xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[] +**** xref:testing/annotations/integration-junit4.adoc[] +**** xref:testing/annotations/integration-junit-jupiter.adoc[] +**** xref:testing/annotations/integration-meta.adoc[] +*** xref:testing/resources.adoc[] * xref:integration.adoc[] ** xref:integration/rest-clients.adoc[] ** xref:integration/jms.adoc[] @@ -419,6 +432,7 @@ *** 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/appendix.adoc[] * xref:languages.adoc[] @@ -437,4 +451,6 @@ ** xref:languages/groovy.adoc[] ** xref:languages/dynamic.adoc[] * xref:appendix.adoc[] -* https://github.com/spring-projects/spring-framework/wiki[Wiki] \ No newline at end of file +* {spring-framework-docs-root}/{spring-version}/javadoc-api/[Java API,window=_blank, role=link-external] +* {spring-framework-api-kdoc}/[Kotlin API,window=_blank, role=link-external] +* {spring-framework-wiki}[Wiki, window=_blank, role=link-external] diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc index c6c7d9d89815..9a8c9048c051 100644 --- a/framework-docs/modules/ROOT/pages/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/appendix.adoc @@ -8,7 +8,7 @@ within the core Spring Framework. [[appendix-spring-properties]] == Spring Properties -{api-spring-framework}/core/SpringProperties.html[`SpringProperties`] is a static holder +{spring-framework-api}/core/SpringProperties.html[`SpringProperties`] is a static holder for properties that control certain low-level aspects of the Spring Framework. Users can configure these properties via JVM system properties or programmatically via the `SpringProperties.setProperty(String key, String value)` method. The latter may be @@ -19,15 +19,57 @@ of the classpath -- for example, deployed within the application's JAR file. The following table lists all currently supported Spring properties. .Supported Spring Properties +[cols="1,1"] |=== | Name | Description +| `spring.aop.ajc.ignore` +| Instructs Spring to ignore ajc-compiled aspects for Spring AOP proxying, restoring traditional +Spring behavior for scenarios where both weaving and AspectJ auto-proxying are enabled. See +{spring-framework-api}++/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.html#IGNORE_AJC_PROPERTY_NAME++[`AbstractAspectJAdvisorFactory`] +for details. + +| `spring.aot.enabled` +| Indicates the application should run with AOT generated artifacts. See +xref:core/aot.adoc[Ahead of Time Optimizations] and +{spring-framework-api}++/aot/AotDetector.html#AOT_ENABLED++[`AotDetector`] +for details. + | `spring.beaninfo.ignore` | Instructs Spring to use the `Introspector.IGNORE_ALL_BEANINFO` mode when calling the JavaBeans `Introspector`. See -{api-spring-framework}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`] +{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`StandardBeanInfoFactory`] +for details. + +| `spring.cache.reactivestreams.ignore` +| Instructs Spring's caching infrastructure to ignore the presence of Reactive Streams, +in particular Reactor's `Mono`/`Flux` in `@Cacheable` method return type declarations. See +{spring-framework-api}++/cache/interceptor/CacheAspectSupport.html#IGNORE_REACTIVESTREAMS_PROPERTY_NAME++[`CacheAspectSupport`] +for details. + +| `spring.classformat.ignore` +| Instructs Spring to ignore class format exceptions during classpath scanning, in +particular for unsupported class file versions. See +{spring-framework-api}++/context/annotation/ClassPathScanningCandidateComponentProvider.html#IGNORE_CLASSFORMAT_PROPERTY_NAME++[`ClassPathScanningCandidateComponentProvider`] for details. +| `spring.context.checkpoint` +| Property that specifies a common context checkpoint. See +xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and +{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#CHECKPOINT_PROPERTY_NAME++[`DefaultLifecycleProcessor`] +for details. + +| `spring.context.exit` +| Property for terminating the JVM when the context reaches a specific phase. See +xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and +{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#EXIT_PROPERTY_NAME++[`DefaultLifecycleProcessor`] +for details. + +| `spring.context.expression.maxLength` +| The maximum length for +xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language] +expressions used in XML bean definitions, `@Value`, etc. + | `spring.expression.compiler.mode` | The mode to use when compiling expressions for the xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring Expression Language]. @@ -36,7 +78,7 @@ xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring | Instructs Spring to ignore operating system environment variables if a Spring `Environment` property -- for example, a placeholder in a configuration String -- isn't resolvable otherwise. See -{api-spring-framework}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`] +{spring-framework-api}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`] for details. | `spring.jdbc.getParameterType.ignore` @@ -47,12 +89,18 @@ See the note in xref:data-access/jdbc/advanced.adoc#jdbc-batch-list[Batch Operat | Instructs Spring to ignore a default JNDI environment, as an optimization for scenarios where nothing is ever to be found for such JNDI fallback searches to begin with, avoiding the repeated JNDI lookup overhead. See -{api-spring-framework}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`] +{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 -{api-spring-framework}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] +{spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] for details. | `spring.test.aot.processing.failOnError` diff --git a/framework-docs/modules/ROOT/pages/attributes.adoc b/framework-docs/modules/ROOT/pages/attributes.adoc deleted file mode 100644 index 177a6ae44107..000000000000 --- a/framework-docs/modules/ROOT/pages/attributes.adoc +++ /dev/null @@ -1,20 +0,0 @@ -// Spring Portfolio -:docs-site: https://docs.spring.io -:docs-spring-boot: {docs-site}/spring-boot/docs/current/reference -:docs-spring-gemfire: {docs-site}/spring-gemfire/docs/current/reference -:docs-spring-security: {docs-site}/spring-security/reference -// spring-asciidoctor-backends Settings -:chomp: default headers packages -:fold: all -// Spring Framework -:docs-spring-framework: {docs-site}/spring-framework/docs/{spring-version} -:api-spring-framework: {docs-spring-framework}/javadoc-api/org/springframework -:docs-java: {docdir}/../../main/java/org/springframework/docs -:docs-kotlin: {docdir}/../../main/kotlin/org/springframework/docs -:docs-resources: {docdir}/../../main/resources -:spring-framework-main-code: https://github.com/spring-projects/spring-framework/tree/main -// Third-party Links -:docs-graalvm: https://www.graalvm.org/22.3/reference-manual -:gh-rsocket: https://github.com/rsocket -:gh-rsocket-extensions: {gh-rsocket}/rsocket/blob/master/Extensions -:gh-rsocket-java: {gh-rsocket}/rsocket-java diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/advice.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/advice.adoc index fd9ecd219a2a..317af250fe94 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/advice.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/advice.adoc @@ -33,11 +33,11 @@ arbitrary advice types. This section describes the basic concepts and standard a [[aop-api-advice-around]] === Interception Around Advice -The most fundamental advice type in Spring is interception around advice. +The most fundamental advice type in Spring is _interception around advice_. -Spring is compliant with the AOP `Alliance` interface for around advice that uses method -interception. Classes that implement `MethodInterceptor` and that implement around advice should also implement the -following interface: +Spring is compliant with the AOP Alliance interface for around advice that uses method +interception. Classes that implement around advice should therefore implement the +following `MethodInterceptor` interface from the `org.aopalliance.intercept` package: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -49,8 +49,8 @@ following interface: The `MethodInvocation` argument to the `invoke()` method exposes the method being invoked, the target join point, the AOP proxy, and the arguments to the method. The -`invoke()` method should return the invocation's result: the return value of the join -point. +`invoke()` method should return the invocation's result: typically the return value of +the join point. The following example shows a simple `MethodInterceptor` implementation: @@ -58,30 +58,30 @@ The following example shows a simple `MethodInterceptor` implementation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class DebugInterceptor implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("Before: invocation=[" + invocation + "]"); - Object rval = invocation.proceed(); + Object result = invocation.proceed(); System.out.println("Invocation returned"); - return rval; + return result; } } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class DebugInterceptor : MethodInterceptor { override fun invoke(invocation: MethodInvocation): Any { println("Before: invocation=[$invocation]") - val rval = invocation.proceed() + val result = invocation.proceed() println("Invocation returned") - return rval + return result } } ---- @@ -105,7 +105,7 @@ currently define pointcut interfaces. [[aop-api-advice-before]] === Before Advice -A simpler advice type is a before advice. This does not need a `MethodInvocation` +A simpler advice type is a _before advice_. This does not need a `MethodInvocation` object, since it is called only before entering the method. The main advantage of a before advice is that there is no need to invoke the `proceed()` @@ -122,10 +122,6 @@ The following listing shows the `MethodBeforeAdvice` interface: } ---- -(Spring's API design would allow for -field before advice, although the usual objects apply to field interception and it is -unlikely for Spring to ever implement it.) - Note that the return type is `void`. Before advice can insert custom behavior before the join point runs but cannot change the return value. If a before advice throws an exception, it stops further execution of the interceptor chain. The exception @@ -139,7 +135,7 @@ The following example shows a before advice in Spring, which counts all method i ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class CountingBeforeAdvice implements MethodBeforeAdvice { @@ -157,7 +153,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CountingBeforeAdvice : MethodBeforeAdvice { @@ -176,10 +172,10 @@ TIP: Before advice can be used with any pointcut. [[aop-api-advice-throws]] === Throws Advice -Throws advice is invoked after the return of the join point if the join point threw +_Throws advice_ is invoked after the return of the join point if the join point threw an exception. Spring offers typed throws advice. Note that this means that the `org.springframework.aop.ThrowsAdvice` interface does not contain any methods. It is a -tag interface identifying that the given object implements one or more typed throws +marker interface identifying that the given object implements one or more typed throws advice methods. These should be in the following form: [source,java,indent=0,subs="verbatim,quotes"] @@ -189,15 +185,16 @@ advice methods. These should be in the following form: Only the last argument is required. The method signatures may have either one or four arguments, depending on whether the advice method is interested in the method and -arguments. The next two listing show classes that are examples of throws advice. +arguments. The next two listings show classes that are examples of throws advice. -The following advice is invoked if a `RemoteException` is thrown (including from subclasses): +The following advice is invoked if a `RemoteException` is thrown (including subclasses of +`RemoteException`): [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class RemoteThrowsAdvice implements ThrowsAdvice { @@ -209,7 +206,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class RemoteThrowsAdvice : ThrowsAdvice { @@ -220,15 +217,15 @@ Kotlin:: ---- ====== -Unlike the preceding -advice, the next example declares four arguments, so that it has access to the invoked method, method -arguments, and target object. The following advice is invoked if a `ServletException` is thrown: +Unlike the preceding advice, the next example declares four arguments, so that it has +access to the invoked method, method arguments, and target object. The following advice +is invoked if a `ServletException` is thrown: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ServletThrowsAdviceWithArguments implements ThrowsAdvice { @@ -240,7 +237,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ServletThrowsAdviceWithArguments : ThrowsAdvice { @@ -259,7 +256,7 @@ methods can be combined in a single class. The following listing shows the final ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static class CombinedThrowsAdvice implements ThrowsAdvice { @@ -275,7 +272,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CombinedThrowsAdvice : ThrowsAdvice { @@ -304,7 +301,7 @@ TIP: Throws advice can be used with any pointcut. [[aop-api-advice-after-returning]] === After Returning Advice -An after returning advice in Spring must implement the +An _after returning advice_ in Spring must implement the `org.springframework.aop.AfterReturningAdvice` interface, which the following listing shows: [source,java,indent=0,subs="verbatim,quotes"] @@ -326,7 +323,7 @@ not thrown exceptions: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class CountingAfterReturningAdvice implements AfterReturningAdvice { @@ -345,7 +342,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CountingAfterReturningAdvice : AfterReturningAdvice { @@ -368,7 +365,7 @@ TIP: After returning advice can be used with any pointcut. [[aop-api-advice-introduction]] === Introduction Advice -Spring treats introduction advice as a special kind of interception advice. +Spring treats _introduction advice_ as a special kind of interception advice. Introduction requires an `IntroductionAdvisor` and an `IntroductionInterceptor` that implement the following interface: @@ -420,7 +417,7 @@ introduce the following interface to one or more objects: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface Lockable { void lock(); @@ -431,7 +428,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- interface Lockable { fun lock() @@ -480,7 +477,7 @@ The following example shows the example `LockMixin` class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable { @@ -510,7 +507,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class LockMixin : DelegatingIntroductionInterceptor(), Lockable { @@ -556,7 +553,7 @@ The following example shows our `LockMixinAdvisor` class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class LockMixinAdvisor extends DefaultIntroductionAdvisor { @@ -568,7 +565,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java) ---- diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/advised.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/advised.adoc index 46932dfa85f4..639379c2e073 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/advised.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/advised.adoc @@ -10,7 +10,7 @@ following methods: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Advisor[] getAdvisors(); @@ -35,7 +35,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun getAdvisors(): Array @@ -90,7 +90,7 @@ manipulating its advice: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Advised advised = (Advised) myObject; Advisor[] advisors = advised.getAdvisors(); @@ -110,7 +110,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val advised = myObject as Advised val advisors = advised.advisors diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/extensibility.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/extensibility.adoc index 8882dfd2da52..6d6f423a6f2e 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/extensibility.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/extensibility.adoc @@ -12,5 +12,5 @@ support for new custom advice types be added without changing the core framework The only constraint on a custom `Advice` type is that it must implement the `org.aopalliance.aop.Advice` marker interface. -See the {api-spring-framework}/aop/framework/adapter/package-summary.html[`org.springframework.aop.framework.adapter`] +See the {spring-framework-api}/aop/framework/adapter/package-summary.html[`org.springframework.aop.framework.adapter`] javadoc for further information. diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc index 6927d1542739..7c15b2de343e 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc @@ -196,16 +196,16 @@ follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Person person = (Person) factory.getBean("person"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val person = factory.getBean("person") as Person; + val person = factory.getBean("person") as Person ---- ====== @@ -291,6 +291,8 @@ to consider: * `final` classes cannot be proxied, because they cannot be extended. * `final` methods cannot be advised, because they cannot be overridden. * `private` methods cannot be advised, because they cannot be overridden. +* Methods that are not visible, typically package private methods in a parent class +from a different package, cannot be advised because they are effectively private. NOTE: There is no need to add CGLIB to your classpath. CGLIB is repackaged and included in the `spring-core` JAR. In other words, CGLIB-based AOP works "out of the box", as do diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/pointcuts.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/pointcuts.adoc index aa1c2726932e..274e9f5e1f78 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/pointcuts.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/pointcuts.adoc @@ -128,18 +128,7 @@ the resulting pointcut is effectively the union of the specified patterns.) The following example shows how to use `JdkRegexpMethodPointcut`: -[source,xml,indent=0,subs="verbatim"] ----- - - - - .*set.* - .*absquatulate - - - ----- +include-code::./JdkRegexpConfiguration[tag=snippet,indent=0] Spring provides a convenience class named `RegexpMethodPointcutAdvisor`, which lets us also reference an `Advice` (remember that an `Advice` can be an interceptor, before advice, @@ -147,21 +136,7 @@ throws advice, and others). Behind the scenes, Spring uses a `JdkRegexpMethodPoi Using `RegexpMethodPointcutAdvisor` simplifies wiring, as the one bean encapsulates both pointcut and advice, as the following example shows: -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - - .*set.* - .*absquatulate - - - ----- +include-code::./RegexpConfiguration[tag=snippet,indent=0] You can use `RegexpMethodPointcutAdvisor` with any `Advice` type. @@ -212,7 +187,7 @@ following example shows how to subclass `StaticMethodMatcherPointcut`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class TestStaticPointcut extends StaticMethodMatcherPointcut { @@ -224,7 +199,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class TestStaticPointcut : StaticMethodMatcherPointcut() { diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/prog.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/prog.adoc index 0247e4a71c61..f8aa774e73b2 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/prog.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/prog.adoc @@ -12,7 +12,7 @@ interceptor and one advisor: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl); factory.addAdvice(myMethodInterceptor); @@ -22,7 +22,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val factory = ProxyFactory(myBusinessInterfaceImpl) factory.addAdvice(myMethodInterceptor) diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc index fdb41e7b4d18..2be131981c0e 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc @@ -38,7 +38,7 @@ You can change the target by using the `swap()` method on HotSwappableTargetSour ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper"); Object oldTarget = swapper.swap(newTarget); @@ -46,7 +46,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val swapper = beanFactory.getBean("swapper") as HotSwappableTargetSource val oldTarget = swapper.swap(newTarget) @@ -119,7 +119,7 @@ The following listing shows an example configuration: Note that the target object (`businessObjectTarget` in the preceding example) must be a prototype. This lets the `PoolingTargetSource` implementation create new instances -of the target to grow the pool as necessary. See the {api-spring-framework}/aop/target/AbstractPoolingTargetSource.html[javadoc of +of the target to grow the pool as necessary. See the {spring-framework-api}/aop/target/AbstractPoolingTargetSource.html[javadoc of `AbstractPoolingTargetSource`] and the concrete subclass you wish to use for information about its properties. `maxSize` is the most basic and is always guaranteed to be present. @@ -152,7 +152,7 @@ The cast is defined as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject"); System.out.println("Max pool size is " + conf.getMaxSize()); @@ -160,7 +160,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val conf = beanFactory.getBean("businessObject") as PoolingConfig println("Max pool size is " + conf.maxSize) @@ -168,7 +168,7 @@ Kotlin:: ====== NOTE: Pooling stateless service objects is not usually necessary. We do not believe it should -be the default choice, as most stateless objects are naturally thread safe, and instance +be the default choice, as most stateless objects are naturally thread-safe, and instance pooling is problematic if resources are cached. Simpler pooling is available by using auto-proxying. You can set the `TargetSource` implementations @@ -221,8 +221,8 @@ NOTE: `ThreadLocal` instances come with serious issues (potentially resulting in incorrectly using them in multi-threaded and multi-classloader environments. You should always consider wrapping a `ThreadLocal` in some other class and never directly use the `ThreadLocal` itself (except in the wrapper class). Also, you should -always remember to correctly set and unset (where the latter simply involves a call to -`ThreadLocal.set(null)`) the resource local to the thread. Unsetting should be done in +always remember to correctly set and unset (where the latter involves a call to +`ThreadLocal.remove()`) the resource local to the thread. Unsetting should be done in any case, since not unsetting it might result in problematic behavior. Spring's `ThreadLocal` support does this for you and should always be considered in favor of using `ThreadLocal` instances without other proper handling code. diff --git a/framework-docs/modules/ROOT/pages/core/aop/aspectj-programmatic.adoc b/framework-docs/modules/ROOT/pages/core/aop/aspectj-programmatic.adoc index 1a243d51c361..cb92225b78ae 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/aspectj-programmatic.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/aspectj-programmatic.adoc @@ -15,7 +15,7 @@ The basic usage for this class is very simple, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- // create a factory that can generate a proxy for the given target object AspectJProxyFactory factory = new AspectJProxyFactory(targetObject); @@ -34,7 +34,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- // create a factory that can generate a proxy for the given target object val factory = AspectJProxyFactory(targetObject) @@ -52,7 +52,7 @@ Kotlin:: ---- ====== -See the {api-spring-framework}/aop/aspectj/annotation/AspectJProxyFactory.html[javadoc] for more information. +See the {spring-framework-api}/aop/aspectj/annotation/AspectJProxyFactory.html[javadoc] for more information. diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc index 952aca1f76c3..4380293f2f1b 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc @@ -4,7 +4,7 @@ @AspectJ refers to a style of declaring aspects as regular Java classes annotated with annotations. The @AspectJ style was introduced by the -https://www.eclipse.org/aspectj[AspectJ project] as part of the AspectJ 5 release. Spring +{aspectj-site}[AspectJ project] as part of the AspectJ 5 release. Spring interprets the same annotations as AspectJ 5, using a library supplied by AspectJ for pointcut parsing and matching. The AOP runtime is still pure Spring AOP, though, and there is no dependency on the AspectJ compiler or weaver. diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/advice.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/advice.adoc index 30f2bc8dc099..5f4164b845ca 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/advice.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/advice.adoc @@ -17,7 +17,7 @@ The following example uses an inline pointcut expression. ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @@ -34,7 +34,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Before @@ -57,7 +57,7 @@ as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @@ -74,7 +74,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Before @@ -101,7 +101,7 @@ You can declare it by using the `@AfterReturning` annotation. ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @@ -118,7 +118,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.AfterReturning @@ -146,7 +146,7 @@ access, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @@ -165,7 +165,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.AfterReturning @@ -204,7 +204,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @@ -221,7 +221,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.AfterThrowing @@ -247,7 +247,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @@ -266,7 +266,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.AfterThrowing @@ -311,7 +311,7 @@ purposes. The following example shows how to use after finally advice: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.After; @@ -328,7 +328,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.After @@ -417,7 +417,7 @@ The following example shows how to use around advice: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; @@ -438,7 +438,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Around @@ -482,7 +482,7 @@ The `JoinPoint` interface provides a number of useful methods: * `getSignature()`: Returns a description of the method that is being advised. * `toString()`: Prints a useful description of the method being advised. -See the https://www.eclipse.org/aspectj/doc/released/runtime-api/org/aspectj/lang/JoinPoint.html[javadoc] for more detail. +See the {aspectj-api}/org/aspectj/lang/JoinPoint.html[javadoc] for more detail. [[aop-ataspectj-advice-params-passing]] === Passing Parameters to Advice @@ -500,7 +500,7 @@ You could write the following: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)") public void validateAccount(Account account) { @@ -510,7 +510,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)") fun validateAccount(account: Account) { @@ -533,7 +533,7 @@ from the advice. This would look as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)") private void accountDataAccessOperation(Account account) {} @@ -546,7 +546,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)") private fun accountDataAccessOperation(account: Account) { @@ -572,7 +572,7 @@ The following shows the definition of the `@Auditable` annotation: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @@ -583,7 +583,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) @@ -597,7 +597,7 @@ The following shows the advice that matches the execution of `@Auditable` method ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") // <1> public void audit(Auditable auditable) { @@ -609,7 +609,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") // <1> fun audit(auditable: Auditable) { @@ -630,7 +630,7 @@ you have a generic type like the following: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public interface Sample { void sampleGenericMethod(T param); @@ -640,7 +640,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- interface Sample { fun sampleGenericMethod(param: T) @@ -656,7 +656,7 @@ tying the advice parameter to the parameter type for which you want to intercept ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") public void beforeSampleMethod(MyType param) { @@ -666,7 +666,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") fun beforeSampleMethod(param: MyType) { @@ -682,7 +682,7 @@ pointcut as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") public void beforeSampleMethod(Collection param) { @@ -692,7 +692,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") fun beforeSampleMethod(param: Collection) { @@ -730,7 +730,7 @@ of determining parameter names, an exception will be thrown. flag for `javac`. Recommended approach on Java 8+. `AspectJAdviceParameterNameDiscoverer` :: Deduces parameter names from the pointcut expression, `returning`, and `throwing` clauses. See the - {api-spring-framework}/aop/aspectj/AspectJAdviceParameterNameDiscoverer.html[javadoc] + {spring-framework-api}/aop/aspectj/AspectJAdviceParameterNameDiscoverer.html[javadoc] for details on the algorithm used. [[aop-ataspectj-advice-params-names-explicit]] @@ -756,7 +756,7 @@ The following example shows how to use the `argNames` attribute: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Before( value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", // <1> @@ -771,7 +771,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Before( value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", // <1> @@ -794,7 +794,7 @@ point object, the `argNames` attribute does not need to include it: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Before( value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", // <1> @@ -809,7 +809,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Before( value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", // <1> @@ -833,7 +833,7 @@ the `argNames` attribute: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Before("com.xyz.Pointcuts.publicMethod()") // <1> public void audit(JoinPoint jp) { @@ -844,7 +844,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Before("com.xyz.Pointcuts.publicMethod()") // <1> fun audit(jp: JoinPoint) { @@ -867,7 +867,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Around("execution(List find*(..)) && " + "com.xyz.CommonPointcuts.inDataAccessLayer() && " + @@ -882,7 +882,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Around("execution(List find*(..)) && " + "com.xyz.CommonPointcuts.inDataAccessLayer() && " + @@ -923,7 +923,7 @@ Each of the distinct advice types of a particular aspect is conceptually meant t to the join point directly. As a consequence, an `@AfterThrowing` advice method is not supposed to receive an exception from an accompanying `@After`/`@AfterReturning` method. -As of Spring Framework 5.2.7, advice methods defined in the same `@Aspect` class that +Advice methods defined in the same `@Aspect` class that need to run at the same join point are assigned precedence based on their advice type in the following order, from highest to lowest precedence: `@Around`, `@Before`, `@After`, `@AfterReturning`, `@AfterThrowing`. Note, however, that an `@After` advice method will diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/aspectj-support.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/aspectj-support.adoc index 6bbddeaddb80..f3dae9e6d17f 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/aspectj-support.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/aspectj-support.adoc @@ -8,54 +8,8 @@ determines that a bean is advised by one or more aspects, it automatically gener a proxy for that bean to intercept method invocations and ensures that advice is run as needed. -The @AspectJ support can be enabled with XML- or Java-style configuration. In either -case, you also need to ensure that AspectJ's `aspectjweaver.jar` library is on the -classpath of your application (version 1.9 or later). This library is available in the -`lib` directory of an AspectJ distribution or from the Maven Central repository. - - -[[aop-enable-aspectj-java]] -== Enabling @AspectJ Support with Java Configuration - -To enable @AspectJ support with Java `@Configuration`, add the `@EnableAspectJAutoProxy` -annotation, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableAspectJAutoProxy - public class AppConfig { - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableAspectJAutoProxy - class AppConfig ----- -====== - -[[aop-enable-aspectj-xml]] -== Enabling @AspectJ Support with XML Configuration - -To enable @AspectJ support with XML-based configuration, use the `aop:aspectj-autoproxy` -element, as the following example shows: - -[source,xml,indent=0,subs="verbatim"] ----- - ----- - -This assumes that you use schema support as described in -xref:core/appendix/xsd-schemas.adoc[XML Schema-based configuration]. -See xref:core/appendix/xsd-schemas.adoc#aop[the AOP schema] for how to -import the tags in the `aop` namespace. - - +The @AspectJ support can be enabled with programmatic or XML configuration. In either +case, you also need to ensure that AspectJ's `org.aspectj:aspectjweaver` library is on the +classpath of your application (version 1.9 or later). +include-code::./ApplicationConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/at-aspectj.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/at-aspectj.adoc index 5e2e40176e6d..4672c2d547b6 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/at-aspectj.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/at-aspectj.adoc @@ -9,43 +9,12 @@ minimal steps required for a not-very-useful aspect. The first of the two examples shows a regular bean definition in the application context that points to a bean class that is annotated with `@Aspect`: -[source,xml,indent=0,subs="verbatim"] ----- - - - ----- +include-code::./ApplicationConfiguration[tag=snippet,indent=0] The second of the two examples shows the `NotVeryUsefulAspect` class definition, which is annotated with `@Aspect`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages",fold="none"] ----- - package com.xyz; - - import org.aspectj.lang.annotation.Aspect; - - @Aspect - public class NotVeryUsefulAspect { - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages",fold="none"] ----- - package com.xyz - - import org.aspectj.lang.annotation.Aspect - - @Aspect - class NotVeryUsefulAspect ----- -====== +include-code::./NotVeryUsefulAspect[tag=snippet,indent=0] Aspects (classes annotated with `@Aspect`) can have methods and fields, the same as any other class. They can also contain pointcut, advice, and introduction (inter-type) diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/example.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/example.adoc index 896086c9282c..6fd5e242bf37 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/example.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/example.adoc @@ -16,93 +16,9 @@ aspect. Because we want to retry the operation, we need to use around advice so that we can call `proceed` multiple times. The following listing shows the basic aspect implementation: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Aspect - public class ConcurrentOperationExecutor implements Ordered { +include-code::./ConcurrentOperationExecutor[tag=snippet,indent=0] - private static final int DEFAULT_MAX_RETRIES = 2; - - private int maxRetries = DEFAULT_MAX_RETRIES; - private int order = 1; - - public void setMaxRetries(int maxRetries) { - this.maxRetries = maxRetries; - } - - public int getOrder() { - return this.order; - } - - public void setOrder(int order) { - this.order = order; - } - - @Around("com.xyz.CommonPointcuts.businessService()") // <1> - public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { - int numAttempts = 0; - PessimisticLockingFailureException lockFailureException; - do { - numAttempts++; - try { - return pjp.proceed(); - } - catch(PessimisticLockingFailureException ex) { - lockFailureException = ex; - } - } while(numAttempts <= this.maxRetries); - throw lockFailureException; - } - } ----- -<1> References the `businessService` named pointcut defined in xref:core/aop/ataspectj/pointcuts.adoc#aop-common-pointcuts[Sharing Named Pointcut Definitions]. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Aspect - class ConcurrentOperationExecutor : Ordered { - - private val DEFAULT_MAX_RETRIES = 2 - private var maxRetries = DEFAULT_MAX_RETRIES - private var order = 1 - - fun setMaxRetries(maxRetries: Int) { - this.maxRetries = maxRetries - } - - override fun getOrder(): Int { - return this.order - } - - fun setOrder(order: Int) { - this.order = order - } - - @Around("com.xyz.CommonPointcuts.businessService()") // <1> - fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? { - var numAttempts = 0 - var lockFailureException: PessimisticLockingFailureException - do { - numAttempts++ - try { - return pjp.proceed() - } catch (ex: PessimisticLockingFailureException) { - lockFailureException = ex - } - - } while (numAttempts <= this.maxRetries) - throw lockFailureException - } - } ----- -<1> References the `businessService` named pointcut defined in xref:core/aop/ataspectj/pointcuts.adoc#aop-common-pointcuts[Sharing Named Pointcut Definitions]. -====== +`@Around("com.xyz.CommonPointcuts.businessService()")` references the `businessService` named pointcut defined in xref:core/aop/ataspectj/pointcuts.adoc#aop-common-pointcuts[Sharing Named Pointcut Definitions]. Note that the aspect implements the `Ordered` interface so that we can set the precedence of the aspect higher than the transaction advice (we want a fresh transaction each time we @@ -114,70 +30,15 @@ we have exhausted all of our retry attempts. The corresponding Spring configuration follows: -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - ----- +include-code::./ApplicationConfiguration[tag=snippet,indent=0] To refine the aspect so that it retries only idempotent operations, we might define the following `Idempotent` annotation: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Retention(RetentionPolicy.RUNTIME) - // marker annotation - public @interface Idempotent { - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Retention(AnnotationRetention.RUNTIME) - // marker annotation - annotation class Idempotent ----- -====== +include-code::./service/Idempotent[tag=snippet,indent=0] We can then use the annotation to annotate the implementation of service operations. The change to the aspect to retry only idempotent operations involves refining the pointcut expression so that only `@Idempotent` operations match, as follows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Around("execution(* com.xyz..service.*.*(..)) && " + - "@annotation(com.xyz.service.Idempotent)") - public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Around("execution(* com.xyz..service.*.*(..)) && " + - "@annotation(com.xyz.service.Idempotent)") - fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? { - // ... - } ----- -====== - - - +include-code::./service/SampleService[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/instantiation-models.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/instantiation-models.adoc index 2e4b54347e17..b8d5eab1b405 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/instantiation-models.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/instantiation-models.adoc @@ -17,7 +17,7 @@ annotation. Consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Aspect("perthis(execution(* com.xyz..service.*.*(..)))") public class MyAspect { @@ -33,7 +33,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Aspect("perthis(execution(* com.xyz..service.*.*(..)))") class MyAspect { diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/introductions.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/introductions.adoc index e8732d3cc3d9..3244d288f9f9 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/introductions.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/introductions.adoc @@ -9,13 +9,13 @@ You can make an introduction by using the `@DeclareParents` annotation. This ann is used to declare that matching types have a new parent (hence the name). For example, given an interface named `UsageTracked` and an implementation of that interface named `DefaultUsageTracked`, the following aspect declares that all implementors of service -interfaces also implement the `UsageTracked` interface (e.g. for statistics via JMX): +interfaces also implement the `UsageTracked` interface (for example, for statistics via JMX): [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Aspect public class UsageTracking { @@ -33,7 +33,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Aspect class UsageTracking { @@ -63,16 +63,16 @@ you would write the following: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- UsageTracked usageTracked = context.getBean("myService", UsageTracked.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- - val usageTracked = context.getBean("myService", UsageTracked.class) + val usageTracked = context.getBean("myService") ---- ====== diff --git a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/pointcuts.adoc b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/pointcuts.adoc index 34386f80393d..35f6b8d1dd2f 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/pointcuts.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/pointcuts.adoc @@ -19,7 +19,7 @@ matches the execution of any method named `transfer`: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Pointcut("execution(* transfer(..))") // the pointcut expression private void anyOldTransfer() {} // the pointcut signature @@ -27,7 +27,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Pointcut("execution(* transfer(..))") // the pointcut expression private fun anyOldTransfer() {} // the pointcut signature @@ -36,9 +36,9 @@ Kotlin:: The pointcut expression that forms the value of the `@Pointcut` annotation is a regular AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see -the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ +the {aspectj-docs-progguide}/index.html[AspectJ Programming Guide] (and, for extensions, the -https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 +{aspectj-docs}/adk15notebook/index.html[AspectJ 5 Developer's Notebook]) or one of the books on AspectJ (such as _Eclipse AspectJ_, by Colyer et al., or _AspectJ in Action_, by Ramnivas Laddad). @@ -150,7 +150,7 @@ pointcut expressions by name. The following example shows three pointcut express ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz; @@ -174,7 +174,7 @@ trading module. Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz @@ -217,7 +217,7 @@ expressions for this purpose. Such a class typically resembles the following ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages",fold="none"] +[source,java,indent=0,subs="verbatim",chomp="-packages",fold="none"] ---- package com.xyz; @@ -279,7 +279,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages",fold="none"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages",fold="none"] ---- package com.xyz @@ -392,7 +392,7 @@ method that takes no parameters, whereas `(..)` matches any number (zero or more The `({asterisk})` pattern matches a method that takes one parameter of any type. `(*,String)` matches a method that takes two parameters. The first can be of any type, while the second must be a `String`. Consult the -https://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html[Language +{aspectj-docs-progguide}/semantics-pointcuts.html[Language Semantics] section of the AspectJ Programming Guide for more information. The following examples show some common pointcut expressions: diff --git a/framework-docs/modules/ROOT/pages/core/aop/choosing.adoc b/framework-docs/modules/ROOT/pages/core/aop/choosing.adoc index d5432fce394a..910d6c027895 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/choosing.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/choosing.adoc @@ -59,7 +59,7 @@ For example, in the @AspectJ style you can write something like the following: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Pointcut("execution(* get*())") public void propertyAccess() {} @@ -73,7 +73,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Pointcut("execution(* get*())") fun propertyAccess() {} diff --git a/framework-docs/modules/ROOT/pages/core/aop/introduction-defn.adoc b/framework-docs/modules/ROOT/pages/core/aop/introduction-defn.adoc index 128d6cb42884..2c87464cb608 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/introduction-defn.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/introduction-defn.adoc @@ -65,7 +65,7 @@ with less potential for errors. For example, you do not need to invoke the `proc method on the `JoinPoint` used for around advice, and, hence, you cannot fail to invoke it. All advice parameters are statically typed so that you work with advice parameters of -the appropriate type (e.g. the type of the return value from a method execution) rather +the appropriate type (for example, the type of the return value from a method execution) rather than `Object` arrays. The concept of join points matched by pointcuts is the key to AOP, which distinguishes diff --git a/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc b/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc index 0187cf3a59e6..3150e07c5df4 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc @@ -6,19 +6,27 @@ target object. JDK dynamic proxies are built into the JDK, whereas CGLIB is a co open-source class definition library (repackaged into `spring-core`). If the target object to be proxied implements at least one interface, a JDK dynamic -proxy is used. All of the interfaces implemented by the target type are proxied. -If the target object does not implement any interfaces, a CGLIB proxy is created. +proxy is used, and all of the interfaces implemented by the target type are proxied. +If the target object does not implement any interfaces, a CGLIB proxy is created which +is a runtime-generated subclass of the target type. If you want to force the use of CGLIB proxying (for example, to proxy every method defined for the target object, not only those implemented by its interfaces), you can do so. However, you should consider the following issues: -* With CGLIB, `final` methods cannot be advised, as they cannot be overridden in - runtime-generated subclasses. -* As of Spring 4.0, the constructor of your proxied object is NOT called twice anymore, - since the CGLIB proxy instance is created through Objenesis. Only if your JVM does - not allow for constructor bypassing, you might see double invocations and - corresponding debug log entries from Spring's AOP support. +* `final` classes cannot be proxied, because they cannot be extended. +* `final` methods cannot be advised, because they cannot be overridden. +* `private` methods cannot be advised, because they cannot be overridden. +* Methods that are not visible – for example, package-private methods in a parent class + from a different package – cannot be advised because they are effectively private. +* The constructor of your proxied object will not be called twice, since the CGLIB proxy + instance is created through Objenesis. However, if your JVM does not allow for + constructor bypassing, you might see double invocations and corresponding debug log + entries from Spring's AOP support. +* Your CGLIB proxy usage may face limitations with the Java Module System. As a typical + case, you cannot create a CGLIB proxy for a class from the `java.lang` package when + deploying on the module path. Such cases require a JVM bootstrap flag + `--add-opens=java.base/java.lang=ALL-UNNAMED` which is not available for modules. To force the use of CGLIB proxies, set the value of the `proxy-target-class` attribute of the `` element to true, as follows: @@ -61,15 +69,14 @@ Spring AOP is proxy-based. It is vitally important that you grasp the semantics what that last statement actually means before you write your own aspects or use any of the Spring AOP-based aspects supplied with the Spring Framework. -Consider first the scenario where you have a plain-vanilla, un-proxied, -nothing-special-about-it, straight object reference, as the following -code snippet shows: +Consider first the scenario where you have a plain-vanilla, un-proxied object reference, +as the following code snippet shows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public class SimplePojo implements Pojo { @@ -86,7 +93,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- class SimplePojo : Pojo { @@ -111,7 +118,7 @@ image::aop-proxy-plain-pojo-call.png[] ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public class Main { @@ -125,7 +132,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun main() { val pojo = SimplePojo() @@ -144,7 +151,7 @@ image::aop-proxy-call.png[] ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public class Main { @@ -162,7 +169,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun main() { val factory = ProxyFactory(SimplePojo()) @@ -183,26 +190,35 @@ the interceptors (advice) that are relevant to that particular method call. Howe once the call has finally reached the target object (the `SimplePojo` reference in this case), any method calls that it may make on itself, such as `this.bar()` or `this.foo()`, are going to be invoked against the `this` reference, and not the proxy. -This has important implications. It means that self-invocation is not going to result -in the advice associated with a method invocation getting a chance to run. - -Okay, so what is to be done about this? The best approach (the term "best" is used -loosely here) is to refactor your code such that the self-invocation does not happen. -This does entail some work on your part, but it is the best, least-invasive approach. -The next approach is absolutely horrendous, and we hesitate to point it out, precisely -because it is so horrendous. You can (painful as it is to us) totally tie the logic -within your class to Spring AOP, as the following example shows: +This has important implications. It means that self invocation is not going to result +in the advice associated with a method invocation getting a chance to run. In other words, +self invocation via an explicit or implicit `this` reference will bypass the advice. + +To address that, you have the following options. + +Avoid self invocation :: + The best approach (the term "best" is used loosely here) is to refactor your code such + that the self invocation does not happen. This does entail some work on your part, but + it is the best, least-invasive approach. +Inject a self reference :: + An alternative approach is to make use of + xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-self-injection[self injection], + and invoke methods on the proxy via the self reference instead of via `this`. +Use `AopContext.currentProxy()` :: + This last approach is highly discouraged, and we hesitate to point it out, in favor of + the previous options. However, as a last resort you can choose to tie the logic within + your class to Spring AOP, as the following example shows. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public class SimplePojo implements Pojo { public void foo() { - // this works, but... gah! + // This works, but it should be avoided if possible. ((Pojo) AopContext.currentProxy()).bar(); } @@ -214,12 +230,12 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- class SimplePojo : Pojo { fun foo() { - // this works, but... gah! + // This works, but it should be avoided if possible. (AopContext.currentProxy() as Pojo).bar() } @@ -230,16 +246,16 @@ Kotlin:: ---- ====== -This totally couples your code to Spring AOP, and it makes the class itself aware of -the fact that it is being used in an AOP context, which flies in the face of AOP. It -also requires some additional configuration when the proxy is being created, as the -following example shows: +The use of `AopContext.currentProxy()` totally couples your code to Spring AOP, and it +makes the class itself aware of the fact that it is being used in an AOP context, which +reduces some of the benefits of AOP. It also requires that the `ProxyFactory` is +configured to expose the proxy, as the following example shows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public class Main { @@ -258,7 +274,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun main() { val factory = ProxyFactory(SimplePojo()) @@ -273,9 +289,6 @@ Kotlin:: ---- ====== -Finally, it must be noted that AspectJ does not have this self-invocation issue because -it is not a proxy-based AOP framework. - - - +NOTE: AspectJ compile-time weaving and load-time weaving do not have this self-invocation +issue because they apply advice within the bytecode instead of via a proxy. diff --git a/framework-docs/modules/ROOT/pages/core/aop/resources.adoc b/framework-docs/modules/ROOT/pages/core/aop/resources.adoc index e95cb1f99559..ab723c056921 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/resources.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/resources.adoc @@ -2,7 +2,7 @@ = Further Resources :page-section-summary-toc: 1 -More information on AspectJ can be found on the https://www.eclipse.org/aspectj[AspectJ website]. +More information on AspectJ can be found on the {aspectj-site}[AspectJ website]. _Eclipse AspectJ_ by Adrian Colyer et. al. (Addison-Wesley, 2005) provides a comprehensive introduction and reference for the AspectJ language. diff --git a/framework-docs/modules/ROOT/pages/core/aop/schema.adoc b/framework-docs/modules/ROOT/pages/core/aop/schema.adoc index c51ad3e976fd..ed66092d2697 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/schema.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/schema.adoc @@ -136,7 +136,7 @@ parameters of the matching names, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public void monitor(Object service) { // ... @@ -145,7 +145,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun monitor(service: Any) { // ... @@ -282,14 +282,14 @@ example, you can declare the method signature as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public void doAccessCheck(Object retVal) {... ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun doAccessCheck(retVal: Any) {... ---- @@ -340,14 +340,14 @@ The type of this parameter constrains matching in the same way as described for ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public void doRecoveryActions(DataAccessException dataAccessEx) {... ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun doRecoveryActions(dataAccessEx: DataAccessException) {... ---- @@ -421,7 +421,7 @@ The implementation of the `doBasicProfiling` advice can be exactly the same as i ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch @@ -433,7 +433,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? { // start stopwatch @@ -475,7 +475,7 @@ some around advice used in conjunction with a number of strongly typed parameter ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz.service; @@ -494,7 +494,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz.service @@ -521,7 +521,7 @@ proceed with the method call. The presence of this parameter is an indication th ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz; @@ -545,7 +545,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz @@ -610,7 +610,7 @@ Consider the following driver script: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public class Boot { @@ -624,7 +624,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun main() { val ctx = ClassPathXmlApplicationContext("beans.xml") @@ -714,7 +714,7 @@ The class that backs the `usageTracking` bean would then contain the following m ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); @@ -723,7 +723,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun recordUsage(usageTracked: UsageTracked) { usageTracked.incrementUseCount() @@ -742,14 +742,14 @@ following: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- UsageTracked usageTracked = context.getBean("myService", UsageTracked.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- val usageTracked = context.getBean("myService", UsageTracked.class) ---- @@ -829,7 +829,7 @@ call `proceed` multiple times. The following listing shows the basic aspect impl ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public class ConcurrentOperationExecutor implements Ordered { @@ -869,7 +869,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- class ConcurrentOperationExecutor : Ordered { @@ -953,7 +953,7 @@ to annotate the implementation of service operations, as the following example s ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Retention(RetentionPolicy.RUNTIME) // marker annotation @@ -963,7 +963,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Retention(AnnotationRetention.RUNTIME) // marker annotation diff --git a/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc b/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc index bdf6d01b7076..548d27257fb2 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc @@ -36,7 +36,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz.domain; @@ -50,7 +50,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz.domain @@ -66,7 +66,7 @@ Kotlin:: When used as a marker interface in this way, Spring configures new instances of the annotated type (`Account`, in this case) by using a bean definition (typically prototype-scoped) with the same name as the fully-qualified type name -(`com.xyz.domain.Account`). Since the default name for a bean is the +(`com.xyz.domain.Account`). Since the default name for a bean defined via XML is the fully-qualified name of its type, a convenient way to declare the prototype definition is to omit the `id` attribute, as the following example shows: @@ -84,7 +84,7 @@ can do so directly in the annotation, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz.domain; @@ -98,7 +98,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz.domain @@ -136,7 +136,7 @@ using Spring in accordance with the properties of the annotation". In this conte "initialization" refers to newly instantiated objects (for example, objects instantiated with the `new` operator) as well as to `Serializable` objects that are undergoing deserialization (for example, through -https://docs.oracle.com/javase/8/docs/api/java/io/Serializable.html[readResolve()]). +{java-api}/java.base/java/io/Serializable.html[readResolve()]). [NOTE] ===== @@ -153,14 +153,14 @@ available for use in the body of the constructors, you need to define this on th ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Configurable(preConstruction = true) ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Configurable(preConstruction = true) ---- @@ -168,51 +168,19 @@ Kotlin:: You can find more information about the language semantics of the various pointcut types in AspectJ -https://www.eclipse.org/aspectj/doc/next/progguide/semantics-joinPoints.html[in this -appendix] of the https://www.eclipse.org/aspectj/doc/next/progguide/index.html[AspectJ -Programming Guide]. +{aspectj-docs-progguide}/semantics-joinPoints.html[in this appendix] of the +{aspectj-docs-progguide}/index.html[AspectJ Programming Guide]. ===== For this to work, the annotated types must be woven with the AspectJ weaver. You can either use a build-time Ant or Maven task to do this (see, for example, the -https://www.eclipse.org/aspectj/doc/released/devguide/antTasks.html[AspectJ Development +{aspectj-docs-devguide}/antTasks.html[AspectJ Development Environment Guide]) or load-time weaving (see xref:core/aop/using-aspectj.adoc#aop-aj-ltw[Load-time Weaving with AspectJ in the Spring Framework]). The `AnnotationBeanConfigurerAspect` itself needs to be configured by Spring (in order to obtain -a reference to the bean factory that is to be used to configure new objects). If you -use Java-based configuration, you can add `@EnableSpringConfigured` to any -`@Configuration` class, as follows: +a reference to the bean factory that is to be used to configure new objects). You can define +the related configuration as follows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableSpringConfigured - public class AppConfig { - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableSpringConfigured - class AppConfig { - } ----- -====== - -If you prefer XML based configuration, the Spring -xref:core/appendix/xsd-schemas.adoc#context[`context` namespace] -defines a convenient `context:spring-configured` element, which you can use as follows: - -[source,xml,indent=0,subs="verbatim"] ----- - ----- +include-code::./ApplicationConfiguration[tag=snippet,indent=0] Instances of `@Configurable` objects created before the aspect has been configured result in a message being issued to the debug log and no configuration of the @@ -399,7 +367,7 @@ The focus of this section is on configuring and using LTW in the specific contex Spring Framework. This section is not a general introduction to LTW. For full details on the specifics of LTW and configuring LTW with only AspectJ (with Spring not being involved at all), see the -https://www.eclipse.org/aspectj/doc/released/devguide/ltw.html[LTW section of the AspectJ +{aspectj-docs-devguide}/ltw.html[LTW section of the AspectJ Development Environment Guide]. The value that the Spring Framework brings to AspectJ LTW is in enabling much @@ -421,7 +389,7 @@ who typically are in charge of the deployment configuration, such as the launch Now that the sales pitch is over, let us first walk through a quick example of AspectJ LTW that uses Spring, followed by detailed specifics about elements introduced in the example. For a complete example, see the -https://github.com/spring-projects/spring-petclinic[Petclinic sample application]. +{petclinic-github-org}/spring-framework-petclinic[Petclinic sample application based on Spring Framework]. [[aop-aj-ltw-first-example]] @@ -445,7 +413,7 @@ It is a time-based profiler that uses the @AspectJ-style of aspect declaration: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz; @@ -478,7 +446,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz @@ -522,8 +490,8 @@ standard AspectJ. The following example shows the `aop.xml` file: - - + + @@ -534,6 +502,11 @@ standard AspectJ. The following example shows the `aop.xml` file: ---- +NOTE: It is recommended to only weave specific classes (typically those in the +application packages, as shown in the `aop.xml` example above) in order +to avoid side effects such as AspectJ dump files and warnings. +This is also a best practice from an efficiency perspective. + Now we can move on to the Spring-specific portion of the configuration. We need to configure a `LoadTimeWeaver` (explained later). This load-time weaver is the essential component responsible for weaving the aspect configuration in one or @@ -571,7 +544,7 @@ driver class with a `main(..)` method to demonstrate the LTW in action: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz; @@ -593,7 +566,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz @@ -621,7 +594,7 @@ java -javaagent:C:/projects/xyz/lib/spring-instrument.jar com.xyz.Main ---- The `-javaagent` is a flag for specifying and enabling -https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html[agents +{java-api}/java.instrument/java/lang/instrument/package-summary.html[agents to instrument programs that run on the JVM]. The Spring Framework ships with such an agent, the `InstrumentationSavingAgent`, which is packaged in the `spring-instrument.jar` that was supplied as the value of the `-javaagent` argument in @@ -652,7 +625,7 @@ result: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz; @@ -674,7 +647,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz @@ -715,13 +688,32 @@ Furthermore, the compiled aspect classes need to be available on the classpath. [[aop-aj-ltw-aop_dot_xml]] -=== 'META-INF/aop.xml' +=== `META-INF/aop.xml` The AspectJ LTW infrastructure is configured by using one or more `META-INF/aop.xml` files that are on the Java classpath (either directly or, more typically, in jar files). +For example: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + + + + + + +---- + +NOTE: It is recommended to only weave specific classes (typically those in the +application packages, as shown in the `aop.xml` example above) in order +to avoid side effects such as AspectJ dump files and warnings. +This is also a best practice from an efficiency perspective. The structure and contents of this file is detailed in the LTW part of the -https://www.eclipse.org/aspectj/doc/released/devguide/ltw-configuration.html[AspectJ reference +{aspectj-docs-devguide}/ltw-configuration.html[AspectJ reference documentation]. Because the `aop.xml` file is 100% AspectJ, we do not describe it further here. @@ -760,52 +752,9 @@ adding one line. (Note that you almost certainly need to use an `ApplicationContext` as your Spring container -- typically, a `BeanFactory` is not enough because the LTW support uses `BeanFactoryPostProcessors`.) -To enable the Spring Framework's LTW support, you need to configure a `LoadTimeWeaver`, -which typically is done by using the `@EnableLoadTimeWeaving` annotation, as follows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableLoadTimeWeaving - public class AppConfig { - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableLoadTimeWeaving - class AppConfig { - } ----- -====== - -Alternatively, if you prefer XML-based configuration, use the -`` element. Note that the element is defined in the -`context` namespace. The following example shows how to use ``: - -[source,xml,indent=0,subs="verbatim"] ----- - - +To enable the Spring Framework's LTW support, you need to configure a `LoadTimeWeaver` as follows: - - - ----- +include-code::./ApplicationConfiguration[tag=snippet,indent=0] The preceding configuration automatically defines and registers a number of LTW-specific infrastructure beans, such as a `LoadTimeWeaver` and an `AspectJWeavingEnabler`, for you. @@ -841,63 +790,12 @@ Note that the table lists only the `LoadTimeWeavers` that are autodetected when use the `DefaultContextLoadTimeWeaver`. You can specify exactly which `LoadTimeWeaver` implementation to use. -To specify a specific `LoadTimeWeaver` with Java configuration, implement the -`LoadTimeWeavingConfigurer` interface and override the `getLoadTimeWeaver()` method. +To configure a specific `LoadTimeWeaver`, implement the +`LoadTimeWeavingConfigurer` interface and override the `getLoadTimeWeaver()` method +(or use the XML equivalent). The following example specifies a `ReflectiveLoadTimeWeaver`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableLoadTimeWeaving - public class AppConfig implements LoadTimeWeavingConfigurer { - - @Override - public LoadTimeWeaver getLoadTimeWeaver() { - return new ReflectiveLoadTimeWeaver(); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableLoadTimeWeaving - class AppConfig : LoadTimeWeavingConfigurer { - - override fun getLoadTimeWeaver(): LoadTimeWeaver { - return ReflectiveLoadTimeWeaver() - } - } ----- -====== - -If you use XML-based configuration, you can specify the fully qualified class name -as the value of the `weaver-class` attribute on the `` -element. Again, the following example specifies a `ReflectiveLoadTimeWeaver`: - -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - ----- +include-code::./CustomWeaverConfiguration[tag=snippet,indent=0] The `LoadTimeWeaver` that is defined and registered by the configuration can be later retrieved from the Spring container by using the well known name, `loadTimeWeaver`. diff --git a/framework-docs/modules/ROOT/pages/core/aot.adoc b/framework-docs/modules/ROOT/pages/core/aot.adoc index 7c2a5ddd0920..80a75965d774 100644 --- a/framework-docs/modules/ROOT/pages/core/aot.adoc +++ b/framework-docs/modules/ROOT/pages/core/aot.adoc @@ -15,10 +15,13 @@ Applying such optimizations early implies the following restrictions: * The classpath is fixed and fully defined at build time. * The beans defined in your application cannot change at runtime, meaning: -** `@Profile`, in particular profile-specific configuration needs to be chosen at build time. +** `@Profile`, in particular profile-specific configuration, needs to be chosen at build time and is automatically enabled at runtime when AOT is enabled. ** `Environment` properties that impact the presence of a bean (`@Conditional`) are only considered at build time. -* Bean definitions with instance suppliers (lambdas or method references) cannot be transformed ahead-of-time (see related https://github.com/spring-projects/spring-framework/issues/29555[spring-framework#29555] issue). -* Make sure that the bean type is as precise as possible. +* Bean definitions with instance suppliers (lambdas or method references) cannot be transformed ahead-of-time. +* Beans registered as singletons (using `registerSingleton`, typically from +`ConfigurableListableBeanFactory`) cannot be transformed ahead-of-time either. +* As we cannot rely on the instance, make sure that the bean type is as precise as +possible. TIP: See also the xref:core/aot.adoc#aot.bestpractices[] section. @@ -27,15 +30,15 @@ A Spring AOT processed application typically generates: * Java source code * Bytecode (usually for dynamic proxies) -* {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] for the use of reflection, resource loading, serialization, and JDK proxies. +* {spring-framework-api}/aot/hint/RuntimeHints.html[`RuntimeHints`] for the use of reflection, resource loading, serialization, and JDK proxies NOTE: At the moment, AOT is focused on allowing Spring applications to be deployed as native images using GraalVM. We intend to support more JVM-based use cases in future generations. [[aot.basics]] -== AOT engine overview +== AOT Engine Overview -The entry point of the AOT engine for processing an `ApplicationContext` arrangement is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {api-spring-framework}/aot/generate/GenerationContext.html[`GenerationContext`]: +The entry point of the AOT engine for processing an `ApplicationContext` is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {spring-framework-api}/aot/generate/GenerationContext.html[`GenerationContext`]: * Refresh an `ApplicationContext` for AOT processing. Contrary to a traditional refresh, this version only creates bean definitions, not bean instances. * Invoke the available `BeanFactoryInitializationAotProcessor` implementations and apply their contributions against the `GenerationContext`. @@ -67,7 +70,14 @@ include-code::./AotProcessingSample[tag=aotcontext] In this mode, xref:core/beans/factory-extension.adoc#beans-factory-extension-factory-postprocessors[`BeanFactoryPostProcessor` implementations] are invoked as usual. This includes configuration class parsing, import selectors, classpath scanning, etc. Such steps make sure that the `BeanRegistry` contains the relevant bean definitions for the application. -If bean definitions are guarded by conditions (such as `@Profile`), these are discarded at this stage. +If bean definitions are guarded by conditions (such as `@Profile`), these are evaluated, +and bean definitions that don't match their conditions are discarded at this stage. + +If custom code needs to register extra beans programmatically, make sure that custom +registration code uses `BeanDefinitionRegistry` instead of `BeanFactory` as only bean +definitions are taken into account. A good pattern is to implement +`ImportBeanDefinitionRegistrar` and register it via an `@Import` on one of your +configuration classes. Because this mode does not actually create bean instances, `BeanPostProcessor` implementations are not invoked, except for specific variants that are relevant for AOT processing. These are: @@ -81,15 +91,15 @@ Once this part completes, the `BeanFactory` contains the bean definitions that a [[aot.bean-factory-initialization-contributions]] == Bean Factory Initialization AOT Contributions -Components that want to participate in this step can implement the {api-spring-framework}/beans/factory/aot/BeanFactoryInitializationAotProcessor.html[`BeanFactoryInitializationAotProcessor`] interface. +Components that want to participate in this step can implement the {spring-framework-api}/beans/factory/aot/BeanFactoryInitializationAotProcessor.html[`BeanFactoryInitializationAotProcessor`] interface. Each implementation can return an AOT contribution, based on the state of the bean factory. -An AOT contribution is a component that contributes generated code that reproduces a particular behavior. +An AOT contribution is a component that contributes generated code which reproduces a particular behavior. It can also contribute `RuntimeHints` to indicate the need for reflection, resource loading, serialization, or JDK proxies. -A `BeanFactoryInitializationAotProcessor` implementation can be registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface. +A `BeanFactoryInitializationAotProcessor` implementation can be registered in `META-INF/spring/aot.factories` with a key equal to the fully-qualified name of the interface. -A `BeanFactoryInitializationAotProcessor` can also be implemented directly by a bean. +The `BeanFactoryInitializationAotProcessor` interface can also be implemented directly by a bean. In this mode, the bean provides an AOT contribution equivalent to the feature it provides with a regular runtime. Consequently, such a bean is automatically excluded from the AOT-optimized context. @@ -111,7 +121,7 @@ This interface is used as follows: * Implemented by a `BeanPostProcessor` bean, to replace its runtime behavior. For instance xref:core/beans/factory-extension.adoc#beans-factory-extension-bpp-examples-aabpp[`AutowiredAnnotationBeanPostProcessor`] implements this interface to generate code that injects members annotated with `@Autowired`. -* Implemented by a type registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface. +* Implemented by a type registered in `META-INF/spring/aot.factories` with a key equal to the fully-qualified name of the interface. Typically used when the bean definition needs to be tuned for specific features of the core framework. [NOTE] @@ -130,7 +140,7 @@ Taking our previous example, let's assume that `DataSourceConfiguration` is as f ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration(proxyBeanMethods = false) public class DataSourceConfiguration { @@ -142,8 +152,23 @@ Java:: } ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Configuration(proxyBeanMethods = false) + class DataSourceConfiguration { + + @Bean + fun dataSource() = SimpleDataSource() + + } +---- ====== +WARNING: Kotlin class names with backticks that use invalid Java identifiers (not starting with a letter, containing spaces, etc.) are not supported. + Since there isn't any particular condition on this class, `dataSourceConfiguration` and `dataSource` are identified as candidates. The AOT engine will convert the configuration class above to code similar to the following: @@ -151,11 +176,12 @@ The AOT engine will convert the configuration class above to code similar to the ====== Java:: + -[source,java,indent=0,role="primary"] +[source,java,indent=0] ---- /** * Bean definitions for {@link DataSourceConfiguration} */ + @Generated public class DataSourceConfiguration__BeanDefinitions { /** * Get the bean definition for 'dataSourceConfiguration' @@ -190,11 +216,25 @@ Java:: NOTE: The exact code generated may differ depending on the exact nature of your bean definitions. +TIP: Each generated class is annotated with `org.springframework.aot.generate.Generated` to +identify them if they need to be excluded, for instance by static analysis tools. + The generated code above creates bean definitions equivalent to the `@Configuration` class, but in a direct way and without the use of reflection if at all possible. There is a bean definition for `dataSourceConfiguration` and one for `dataSourceBean`. When a `datasource` instance is required, a `BeanInstanceSupplier` is called. This supplier invokes the `dataSource()` method on the `dataSourceConfiguration` bean. +[[aot.running]] +== Running with AOT Optimizations + +AOT is a mandatory step to transform a Spring application to a native executable, so it +is automatically enabled when running in this mode. It is possible to use those optimizations +on the JVM by setting the `spring.aot.enabled` System property to `true`. + +NOTE: When AOT optimizations are included, some decisions that have been taken at build-time +are hard-coded in the application setup. For instance, profiles that have been enabled at +build-time are automatically enabled at runtime as well. + [[aot.bestpractices]] == Best Practices @@ -203,11 +243,33 @@ However, keep in mind that some optimizations are made at build time based on a This section lists the best practices that make sure your application is ready for AOT. +[[aot.bestpractices.bean-registration]] +=== Programmatic Bean Registration + +The AOT engine takes care of the `@Configuration` model and any callback that might be +invoked as part of processing your configuration. If you need to register additional +beans programmatically, make sure to use a `BeanDefinitionRegistry` to register +bean definitions. + +This can typically be done via a `BeanDefinitionRegistryPostProcessor`. Note that, if it +is registered itself as a bean, it will be invoked again at runtime unless you make +sure to implement `BeanFactoryInitializationAotProcessor` as well. A more idiomatic +way is to implement `ImportBeanDefinitionRegistrar` and register it using `@Import` on +one of your configuration classes. This invokes your custom code as part of configuration +class parsing. + +If you declare additional beans programmatically using a different callback, they are +likely not going to be handled by the AOT engine, and therefore no hints are going to be +generated for them. Depending on the environment, those beans may not be registered at +all. For instance, classpath scanning does not work in a native image as there is no +notion of a classpath. For cases like this, it is crucial that the scanning happens at +build time. + [[aot.bestpractices.bean-type]] -=== Expose The Most Precise Bean Type +=== Expose the Most Precise Bean Type While your application may interact with an interface that a bean implements, it is still very important to declare the most precise type. -The AOT engine performs additional checks on the bean type, such as detecting the presence of `@Autowired` members, or lifecycle callback methods. +The AOT engine performs additional checks on the bean type, such as detecting the presence of `@Autowired` members or lifecycle callback methods. For `@Configuration` classes, make sure that the return type of the factory `@Bean` method is as precise as possible. Consider the following example: @@ -216,7 +278,7 @@ Consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration(proxyBeanMethods = false) public class UserConfiguration { @@ -228,6 +290,19 @@ Java:: } ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Configuration(proxyBeanMethods = false) + class UserConfiguration { + + @Bean + fun myInterface(): MyInterface = MyImplementation() + + } +---- ====== In the example above, the declared type for the `myInterface` bean is `MyInterface`. @@ -240,7 +315,7 @@ The example above should be rewritten as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration(proxyBeanMethods = false) public class UserConfiguration { @@ -252,16 +327,70 @@ Java:: } ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Configuration(proxyBeanMethods = false) + class UserConfiguration { + + @Bean + fun myInterface() = MyImplementation() + + } +---- ====== If you are registering bean definitions programmatically, consider using `RootBeanBefinition` as it allows to specify a `ResolvableType` that handles generics. [[aot.bestpractices.constructors]] === Avoid Multiple Constructors + The container is able to choose the most appropriate constructor to use based on several candidates. However, this is not a best practice and flagging the preferred constructor with `@Autowired` if necessary is preferred. -In case you are working on a code base that you can't modify, you can set the {api-spring-framework}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used. +In case you are working on a code base that you cannot modify, you can set the {spring-framework-api}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used. + +[[aot.bestpractices.complex-data-structures]] +=== Avoid Complex Data Structures for Constructor Parameters and Properties + +When crafting a `RootBeanDefinition` programmatically, you are not constrained in terms of types that you can use. +For instance, you may have a custom `record` with several properties that your bean takes as a constructor argument. + +While this works fine with the regular runtime, AOT does not know how to generate the code of your custom data structure. +A good rule of thumb is to keep in mind that bean definitions are an abstraction on top of several models. +Rather than using such structures, decomposing to simple types or referring to a bean that is built as such is recommended. + +As a last resort, you can implement your own `org.springframework.aot.generate.ValueCodeGenerator$Delegate`. +To use it, register its fully qualified name in `META-INF/spring/aot.factories` using the `Delegate` as the key. + +[[aot.bestpractices.custom-arguments]] +=== Avoid Creating Beans with Custom Arguments + +Spring AOT detects what needs to be done to create a bean and translates that in generated code using an instance supplier. +The container also supports creating a bean with {spring-framework-api}++/beans/factory/BeanFactory.html#getBean(java.lang.String,java.lang.Object...)++[custom arguments] that leads to several issues with AOT: + +. The custom arguments require dynamic introspection of a matching constructor or factory method. +Those arguments cannot be detected by AOT, so the necessary reflection hints will have to be provided manually. +. By-passing the instance supplier means that all other optimizations after creation are skipped as well. +For instance, autowiring on fields and methods will be skipped as they are handled in the instance supplier. + +Rather than having prototype-scoped beans created with custom arguments, we recommend a manual factory pattern where a bean is responsible for the creation of the instance. + +[[aot.bestpractices.circular-dependencies]] +=== Avoid Circular Dependencies + +Certain use cases can result in circular dependencies between one or more beans. With the +regular runtime, it may be possible to wire those circular dependencies via `@Autowired` +on setter methods or fields. However, an AOT-optimized context will fail to start with +explicit circular dependencies. + +In an AOT-optimized application, you should therefore strive to avoid circular +dependencies. If that is not possible, you can use `@Lazy` injection points or +`ObjectProvider` to lazily access or retrieve the necessary collaborating beans. See +xref:core/beans/classpath-scanning.adoc#beans-factorybeans-annotations-lazy-injection-points[this tip] +for further information. [[aot.bestpractices.factory-bean]] === FactoryBean @@ -276,10 +405,19 @@ Consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ClientFactoryBean implements FactoryBean { + // ... + } +---- +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + class ClientFactoryBean : FactoryBean { + // ... } ---- ====== @@ -290,7 +428,7 @@ A concrete client declaration should provide a resolved generic for the client, ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration(proxyBeanMethods = false) public class UserConfiguration { @@ -302,6 +440,19 @@ Java:: } ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Configuration(proxyBeanMethods = false) + class UserConfiguration { + + @Bean + fun myClient() = ClientFactoryBean(...) + + } +---- ====== If the `FactoryBean` bean definition is registered programmatically, make sure to follow these steps: @@ -316,13 +467,23 @@ The following example showcases a basic definition: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[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); ---- + +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) +---- ====== [[aot.bestpractices.jpa]] @@ -334,7 +495,7 @@ The JPA persistence unit has to be known upfront for certain optimizations to ap ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) { @@ -344,6 +505,19 @@ Java:: return factoryBean; } ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Bean + fun customDBEntityManagerFactory(dataSource: DataSource): LocalContainerEntityManagerFactoryBean { + val factoryBean = LocalContainerEntityManagerFactoryBean() + factoryBean.dataSource = dataSource + factoryBean.setPackagesToScan("com.example.app") + return factoryBean + } +---- ====== To make sure the scanning occurs ahead of time, a `PersistenceManagedTypes` bean must be declared and used by the @@ -353,7 +527,7 @@ factory bean definition, as shown by the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) { @@ -369,6 +543,25 @@ Java:: return factoryBean; } ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Bean + fun persistenceManagedTypes(resourceLoader: ResourceLoader): PersistenceManagedTypes { + return PersistenceManagedTypesScanner(resourceLoader) + .scan("com.example.app") + } + + @Bean + fun customDBEntityManagerFactory(dataSource: DataSource, managedTypes: PersistenceManagedTypes): LocalContainerEntityManagerFactoryBean { + val factoryBean = LocalContainerEntityManagerFactoryBean() + factoryBean.dataSource = dataSource + factoryBean.setManagedTypes(managedTypes) + return factoryBean + } +---- ====== [[aot.hints]] @@ -376,20 +569,27 @@ Java:: Running an application as a native image requires additional information compared to a regular JVM runtime. For instance, GraalVM needs to know ahead of time if a component uses reflection. -Similarly, classpath resources are not shipped in a native image unless specified explicitly. +Similarly, classpath resources are not included in a native image unless specified explicitly. Consequently, if the application needs to load a resource, it must be referenced from the corresponding GraalVM native image configuration file. -The {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] API collects the need for reflection, resource loading, serialization, and JDK proxies at runtime. +The {spring-framework-api}/aot/hint/RuntimeHints.html[`RuntimeHints`] API collects the need for reflection, resource loading, serialization, and JDK proxies at runtime. The following example makes sure that `config/app.properties` can be loaded from the classpath at runtime within a native image: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- runtimeHints.resources().registerPattern("config/app.properties"); ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + runtimeHints.resources().registerPattern("config/app.properties") +---- ====== A number of contracts are handled automatically during AOT processing. @@ -411,49 +611,54 @@ include-code::./SpellCheckService[] If at all possible, `@ImportRuntimeHints` should be used as close as possible to the component that requires the hints. This way, if the component is not contributed to the `BeanFactory`, the hints won't be contributed either. -It is also possible to register an implementation statically by adding an entry in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the `RuntimeHintsRegistrar` interface. +It is also possible to register an implementation statically by adding an entry in `META-INF/spring/aot.factories` with a key equal to the fully-qualified name of the `RuntimeHintsRegistrar` interface. [[aot.hints.reflective]] === `@Reflective` -{api-spring-framework}/aot/hint/annotation/Reflective.html[`@Reflective`] provides an idiomatic way to flag the need for reflection on an annotated element. +{spring-framework-api}/aot/hint/annotation/Reflective.html[`@Reflective`] provides an idiomatic way to flag the need for reflection on an annotated element. For instance, `@EventListener` is meta-annotated with `@Reflective` since the underlying implementation invokes the annotated method using reflection. -By default, only Spring beans are considered and an invocation hint is registered for the annotated element. -This can be tuned by specifying a custom `ReflectiveProcessor` implementation via the -`@Reflective` annotation. +Out-of-the-box, only Spring beans are considered but you can opt-in for scanning using `@ReflectiveScan`. +In the example below, all types of the package `com.example.app` and their subpackages are considered: + +include-code::./MyConfiguration[] + +Scanning happens during AOT processing and the types in the target packages do not need to have a class-level annotation to be considered. +This performs a "deep scan" and the presence of `@Reflective`, either directly or as a meta-annotation, is checked on types, fields, constructors, methods, and enclosed elements. + +By default, `@Reflective` registers an invocation hint for the annotated element. +This can be tuned by specifying a custom `ReflectiveProcessor` implementation via the `@Reflective` annotation. Library authors can reuse this annotation for their own purposes. -If components other than Spring beans need to be processed, a `BeanFactoryInitializationAotProcessor` can detect the relevant types and use `ReflectiveRuntimeHintsRegistrar` to process them. +An example of such customization is covered in the next section. -[[aot.hints.register-reflection-for-binding]] -=== `@RegisterReflectionForBinding` +[[aot.hints.register-reflection]] +=== `@RegisterReflection` -{api-spring-framework}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is a specialization of `@Reflective` that registers the need for serializing arbitrary types. -A typical use case is the use of DTOs that the container cannot infer, such as using a web client within a method body. +{spring-framework-api}/aot/hint/annotation/RegisterReflection.html[`@RegisterReflection`] is a specialization of `@Reflective` that provides a declarative way of registering reflection for arbitrary types. -`@RegisterReflectionForBinding` can be applied to any Spring bean at the class level, but it can also be applied directly to a method, field, or constructor to better indicate where the hints are actually required. -The following example registers `Account` for serialization. +NOTE: As a specialization of `@Reflective`, this is also detected if you're using `@ReflectiveScan`. -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Component - public class OrderService { +In the following example, public constructors and public methods can be invoked via reflection on `AccountService`: - @RegisterReflectionForBinding(Account.class) - public void process(Order order) { - // ... - } +include-code::./MyConfiguration[tag=snippet,indent=0] - } ----- -====== +`@RegisterReflection` can be applied to any target type at the class level, but it can also be applied directly to a method to better indicate where the hints are actually required. + +`@RegisterReflection` can be used as a meta-annotation to provide more specific needs. +{spring-framework-api}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is such composed annotation and registers the need for serializing arbitrary types. +A typical use case is the use of DTOs that the container cannot infer, such as using a web client within a method body. + +The following example registers `Order` for serialization. + +include-code::./OrderService[tag=snippet,indent=0] + +This registers hints for constructors, fields, properties, and record components of `Order`. +Hints are also registered for types transitively used on properties and record components. +In other words, if `Order` exposes others types, hints are registered for those as well. [[aot.hints.testing]] === Testing Runtime Hints @@ -467,7 +672,7 @@ include-code::./SpellCheckServiceTests[tag=hintspredicates] With `RuntimeHintsPredicates`, we can check for reflection, resource, serialization, or proxy generation hints. This approach works well for unit tests but implies that the runtime behavior of a component is well known. -You can learn more about the global runtime behavior of an application by running its test suite (or the app itself) with the {docs-graalvm}/native-image/metadata/AutomaticMetadataCollection/[GraalVM tracing agent]. +You can learn more about the global runtime behavior of an application by running its test suite (or the app itself) with the {graalvm-docs}/native-image/metadata/AutomaticMetadataCollection/[GraalVM tracing agent]. This agent will record all relevant calls requiring GraalVM hints at runtime and write them out as JSON configuration files. For more targeted discovery and testing, Spring Framework ships a dedicated module with core AOT testing utilities, `"org.springframework:spring-core-test"`. @@ -485,7 +690,7 @@ If you forgot to contribute a hint, the test will fail and provide some details [source,txt,indent=0,subs="verbatim,quotes"] ---- org.springframework.docs.core.aot.hints.testing.SampleReflection performReflection -INFO: Spring version:6.0.0-SNAPSHOT +INFO: Spring version: 6.2.0 Missing <"ReflectionHints"> for invocation with arguments ["org.springframework.core.SpringVersion", @@ -499,4 +704,4 @@ io.spring.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldReg There are various ways to configure this Java agent in your build, so please refer to the documentation of your build tool and test execution plugin. The agent itself can be configured to instrument specific packages (by default, only `org.springframework` is instrumented). -You'll find more details in the {spring-framework-main-code}/buildSrc/README.md[Spring Framework `buildSrc` README] file. +You'll find more details in the {spring-framework-code}/buildSrc/README.md[Spring Framework `buildSrc` README] file. diff --git a/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc b/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc index a0ccd3cb33b4..c029f5dd7e08 100644 --- a/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc +++ b/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc @@ -145,7 +145,7 @@ use the `NamespaceHandlerSupport` class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.springframework.samples.xml; @@ -161,7 +161,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.springframework.samples.xml @@ -202,7 +202,7 @@ we can parse our custom XML content, as you can see in the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.springframework.samples.xml; @@ -240,7 +240,7 @@ single `BeanDefinition` represents. Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.springframework.samples.xml @@ -416,7 +416,7 @@ The following listing shows the `Component` class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo; @@ -449,7 +449,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo @@ -480,7 +480,7 @@ setter property for the `components` property. The following listing shows such ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo; @@ -522,7 +522,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo @@ -598,7 +598,7 @@ we then create a custom `NamespaceHandler`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo; @@ -614,7 +614,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo @@ -637,7 +637,7 @@ listing shows our custom `BeanDefinitionParser` implementation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo; @@ -688,7 +688,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo @@ -765,7 +765,7 @@ want to add an additional attribute to the existing bean definition element. By way of another example, suppose that you define a bean definition for a service object that (unknown to it) accesses a clustered -https://jcp.org/en/jsr/detail?id=107[JCache], and you want to ensure that the +{JSR}107[JCache], and you want to ensure that the named JCache instance is eagerly started within the surrounding cluster. The following listing shows such a definition: @@ -787,7 +787,7 @@ JCache-initializing `BeanDefinition`. The following listing shows our `JCacheIni ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo; @@ -807,7 +807,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo @@ -843,7 +843,7 @@ Next, we need to create the associated `NamespaceHandler`, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo; @@ -861,7 +861,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo @@ -886,7 +886,7 @@ The following listing shows our `BeanDefinitionDecorator` implementation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo; @@ -942,7 +942,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo diff --git a/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc b/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc index 38bccafbe276..55b3141dda08 100644 --- a/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc +++ b/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc @@ -66,13 +66,13 @@ developer's intent ("`inject this constant value`"), and it reads better: [[xsd-schemas-util-frfb]] ==== Setting a Bean Property or Constructor Argument from a Field Value -{api-spring-framework}/beans/factory/config/FieldRetrievingFactoryBean.html[`FieldRetrievingFactoryBean`] +{spring-framework-api}/beans/factory/config/FieldRetrievingFactoryBean.html[`FieldRetrievingFactoryBean`] is a `FactoryBean` that retrieves a `static` or non-static field value. It is typically used for retrieving `public` `static` `final` constants, which may then be used to set a property value or constructor argument for another bean. The following example shows how a `static` field is exposed, by using the -{api-spring-framework}/beans/factory/config/FieldRetrievingFactoryBean.html#setStaticField(java.lang.String)[`staticField`] +{spring-framework-api}/beans/factory/config/FieldRetrievingFactoryBean.html#setStaticField(java.lang.String)[`staticField`] property: [source,xml,indent=0,subs="verbatim,quotes"] @@ -109,7 +109,7 @@ to be specified for the bean reference, as the following example shows: You can also access a non-static (instance) field of another bean, as described in the API documentation for the -{api-spring-framework}/beans/factory/config/FieldRetrievingFactoryBean.html[`FieldRetrievingFactoryBean`] +{spring-framework-api}/beans/factory/config/FieldRetrievingFactoryBean.html[`FieldRetrievingFactoryBean`] class. Injecting enumeration values into beans as either property or constructor arguments is @@ -121,7 +121,7 @@ The following example enumeration shows how easy injecting an enum value is: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package jakarta.persistence; @@ -134,7 +134,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package jakarta.persistence @@ -152,7 +152,7 @@ Now consider the following setter of type `PersistenceContextType` and the corre ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package example; @@ -168,7 +168,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package example diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc index c369aaa71514..33e48c41a236 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc @@ -1,33 +1,16 @@ [[beans-annotation-config]] = Annotation-based Container Configuration -.Are annotations better than XML for configuring Spring? -**** -The introduction of annotation-based configuration raised the question of whether this -approach is "`better`" than XML. The short answer is "`it depends.`" The long answer is -that each approach has its pros and cons, and, usually, it is up to the developer to -decide which strategy suits them better. Due to the way they are defined, annotations -provide a lot of context in their declaration, leading to shorter and more concise -configuration. However, XML excels at wiring up components without touching their source -code or recompiling them. Some developers prefer having the wiring close to the source -while others argue that annotated classes are no longer POJOs and, furthermore, that the -configuration becomes decentralized and harder to control. +Spring provides comprehensive support for annotation-based configuration, operating on +metadata in the component class itself by using annotations on the relevant class, +method, or field declaration. As mentioned in +xref:core/beans/factory-extension.adoc#beans-factory-extension-bpp-examples-aabpp[Example: The `AutowiredAnnotationBeanPostProcessor`], +Spring uses `BeanPostProcessors` in conjunction with annotations to make the core IOC +container aware of specific annotations. -No matter the choice, Spring can accommodate both styles and even mix them together. -It is worth pointing out that through its xref:core/beans/java.adoc[JavaConfig] option, Spring lets -annotations be used in a non-invasive way, without touching the target components' -source code and that, in terms of tooling, all configuration styles are supported by -https://spring.io/tools[Spring Tools] for Eclipse, Visual Studio Code, and Theia. -**** - -An alternative to XML setup is provided by annotation-based configuration, which relies -on bytecode metadata for wiring up components instead of XML declarations. Instead of -using XML to describe a bean wiring, the developer moves the configuration into the -component class itself by using annotations on the relevant class, method, or field -declaration. As mentioned in xref:core/beans/factory-extension.adoc#beans-factory-extension-bpp-examples-aabpp[Example: The `AutowiredAnnotationBeanPostProcessor`], using a -`BeanPostProcessor` in conjunction with annotations is a common means of extending the -Spring IoC container. For example, the xref:core/beans/annotation-config/autowired.adoc[`@Autowired`] -annotation provides the same capabilities as described in xref:core/beans/dependencies/factory-autowire.adoc[Autowiring Collaborators] but +For example, the xref:core/beans/annotation-config/autowired.adoc[`@Autowired`] +annotation provides the same capabilities as described in +xref:core/beans/dependencies/factory-autowire.adoc[Autowiring Collaborators] but with more fine-grained control and wider applicability. In addition, Spring provides support for JSR-250 annotations, such as `@PostConstruct` and `@PreDestroy`, as well as support for JSR-330 (Dependency Injection for Java) annotations contained in the @@ -36,13 +19,16 @@ can be found in the xref:core/beans/standard-annotations.adoc[relevant section]. [NOTE] ==== -Annotation injection is performed before XML injection. Thus, the XML configuration -overrides the annotations for properties wired through both approaches. +Annotation injection is performed before external property injection. Thus, external +configuration (for example, XML-specified bean properties) effectively overrides the annotations +for properties when wired through mixed approaches. ==== -As always, you can register the post-processors as individual bean definitions, but they -can also be implicitly registered by including the following tag in an XML-based Spring -configuration (notice the inclusion of the `context` namespace): +Technically, you can register the post-processors as individual bean definitions, but they +are implicitly registered in an `AnnotationConfigApplicationContext` already. + +In an XML-based Spring setup, you may include the following configuration tag to enable +mixing and matching with annotation-based configuration: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -62,11 +48,11 @@ configuration (notice the inclusion of the `context` namespace): The `` element implicitly registers the following post-processors: -* {api-spring-framework}/context/annotation/ConfigurationClassPostProcessor.html[`ConfigurationClassPostProcessor`] -* {api-spring-framework}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`] -* {api-spring-framework}/context/annotation/CommonAnnotationBeanPostProcessor.html[`CommonAnnotationBeanPostProcessor`] -* {api-spring-framework}/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html[`PersistenceAnnotationBeanPostProcessor`] -* {api-spring-framework}/context/event/EventListenerMethodProcessor.html[`EventListenerMethodProcessor`] +* {spring-framework-api}/context/annotation/ConfigurationClassPostProcessor.html[`ConfigurationClassPostProcessor`] +* {spring-framework-api}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`] +* {spring-framework-api}/context/annotation/CommonAnnotationBeanPostProcessor.html[`CommonAnnotationBeanPostProcessor`] +* {spring-framework-api}/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html[`PersistenceAnnotationBeanPostProcessor`] +* {spring-framework-api}/context/event/EventListenerMethodProcessor.html[`EventListenerMethodProcessor`] [NOTE] ==== diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-primary.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-primary.adoc index dcaa7bce6234..af63a56aa661 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-primary.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-primary.adoc @@ -1,5 +1,5 @@ [[beans-autowired-annotation-primary]] -= Fine-tuning Annotation-based Autowiring with `@Primary` += Fine-tuning Annotation-based Autowiring with `@Primary` or `@Fallback` Because autowiring by type may lead to multiple candidates, it is often necessary to have more control over the selection process. One way to accomplish this is with Spring's @@ -15,7 +15,7 @@ primary `MovieCatalog`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class MovieConfiguration { @@ -33,7 +33,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class MovieConfiguration { @@ -50,14 +50,57 @@ Kotlin:: ---- ====== -With the preceding configuration, the following `MovieRecommender` is autowired with the -`firstMovieCatalog`: +Alternatively, as of 6.2, there is a `@Fallback` annotation for demarcating +any beans other than the regular ones to be injected. If only one regular +bean is left, it is effectively primary as well: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + public class MovieConfiguration { + + @Bean + public MovieCatalog firstMovieCatalog() { ... } + + @Bean + @Fallback + public MovieCatalog secondMovieCatalog() { ... } + + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Configuration + class MovieConfiguration { + + @Bean + fun firstMovieCatalog(): MovieCatalog { ... } + + @Bean + @Fallback + fun secondMovieCatalog(): MovieCatalog { ... } + + // ... + } +---- +====== + +With both variants of the preceding configuration, the following +`MovieRecommender` is autowired with the `firstMovieCatalog`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -70,7 +113,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc index 946fb6a317b2..07073d269281 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc @@ -1,19 +1,20 @@ [[beans-autowired-annotation-qualifiers]] = Fine-tuning Annotation-based Autowiring with Qualifiers -`@Primary` is an effective way to use autowiring by type with several instances when one -primary candidate can be determined. When you need more control over the selection process, -you can use Spring's `@Qualifier` annotation. You can associate qualifier values -with specific arguments, narrowing the set of type matches so that a specific bean is -chosen for each argument. In the simplest case, this can be a plain descriptive value, as -shown in the following example: +`@Primary` and `@Fallback` are effective ways to use autowiring by type with several +instances when one primary (or non-fallback) candidate can be determined. + +When you need more control over the selection process, you can use Spring's `@Qualifier` +annotation. You can associate qualifier values with specific arguments, narrowing the set +of type matches so that a specific bean is chosen for each argument. In the simplest case, +this can be a plain descriptive value, as shown in the following example: -- [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -27,7 +28,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -49,7 +50,7 @@ method parameters, as shown in the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -70,7 +71,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -153,48 +154,35 @@ Letting qualifier values select against target bean names, within the type-match candidates, does not require a `@Qualifier` annotation at the injection point. If there is no other resolution indicator (such as a qualifier or a primary marker), for a non-unique dependency situation, Spring matches the injection point name -(that is, the field name or parameter name) against the target bean names and chooses the -same-named candidate, if any. +(that is, the field name or parameter name) against the target bean names and chooses +the same-named candidate, if any (either by bean name or by associated alias). + +Since version 6.1, this requires the `-parameters` Java compiler flag to be present. +As of 6.2, the container applies fast shortcut resolution for bean name matches, +bypassing the full type matching algorithm when the parameter name matches the +bean name and no type, qualifier or primary conditions override the match. It is +therefore recommendable for your parameter names to match the target bean names. ==== -That said, if you intend to express annotation-driven injection by name, do not -primarily use `@Autowired`, even if it is capable of selecting by bean name among -type-matching candidates. Instead, use the JSR-250 `@Resource` annotation, which is -semantically defined to identify a specific target component by its unique name, with -the declared type being irrelevant for the matching process. `@Autowired` has rather -different semantics: After selecting candidate beans by type, the specified `String` +As an alternative for injection by name, consider the JSR-250 `@Resource` annotation +which is semantically defined to identify a specific target component by its unique name, +with the declared type being irrelevant for the matching process. `@Autowired` has rather +different semantics: after selecting candidate beans by type, the specified `String` qualifier value is considered within those type-selected candidates only (for example, matching an `account` qualifier against beans marked with the same qualifier label). For beans that are themselves defined as a collection, `Map`, or array type, `@Resource` is a fine solution, referring to the specific collection or array bean by unique name. -That said, as of 4.3, you can match collection, `Map`, and array types through Spring's +That said, you can match collection, `Map`, and array types through Spring's `@Autowired` type matching algorithm as well, as long as the element type information is preserved in `@Bean` return type signatures or collection inheritance hierarchies. In this case, you can use qualifier values to select among same-typed collections, as outlined in the previous paragraph. -As of 4.3, `@Autowired` also considers self references for injection (that is, references -back to the bean that is currently injected). Note that self injection is a fallback. -Regular dependencies on other components always have precedence. In that sense, self -references do not participate in regular candidate selection and are therefore in -particular never primary. On the contrary, they always end up as lowest precedence. -In practice, you should use self references as a last resort only (for example, for -calling other methods on the same instance through the bean's transactional proxy). -Consider factoring out the affected methods to a separate delegate bean in such a scenario. -Alternatively, you can use `@Resource`, which may obtain a proxy back to the current bean -by its unique name. - -[NOTE] -==== -Trying to inject the results from `@Bean` methods on the same configuration class is -effectively a self-reference scenario as well. Either lazily resolve such references -in the method signature where it is actually needed (as opposed to an autowired field -in the configuration class) or declare the affected `@Bean` methods as `static`, -decoupling them from the containing configuration class instance and its lifecycle. -Otherwise, such beans are only considered in the fallback phase, with matching beans -on other configuration classes selected as primary candidates instead (if available). -==== +`@Autowired` also considers self references for injection (that is, references back to +the bean that is currently injected). See +xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-self-injection[Self Injection] +for details. `@Autowired` applies to fields, constructors, and multi-argument methods, allowing for narrowing through qualifier annotations at the parameter level. In contrast, `@Resource` @@ -210,7 +198,7 @@ provide the `@Qualifier` annotation within your definition, as the following exa ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @@ -223,7 +211,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) @@ -241,7 +229,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -262,7 +250,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -334,7 +322,7 @@ the simple annotation, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @@ -345,7 +333,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) @@ -363,7 +351,7 @@ following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -378,7 +366,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -418,7 +406,7 @@ consider the following annotation definition: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @@ -433,7 +421,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) @@ -450,7 +438,7 @@ In this case `Format` is an enum, defined as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public enum Format { VHS, DVD, BLURAY @@ -459,7 +447,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- enum class Format { VHS, DVD, BLURAY @@ -476,7 +464,7 @@ for both attributes: `genre` and `format`, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -502,7 +490,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc index 02e6a3d2ceae..015e73b748f2 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc @@ -13,7 +13,7 @@ You can apply the `@Autowired` annotation to constructors, as the following exam ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -30,7 +30,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender @Autowired constructor( private val customerPreferenceDao: CustomerPreferenceDao) @@ -54,7 +54,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleMovieLister { @@ -71,7 +71,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -91,7 +91,7 @@ arguments, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -112,7 +112,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -139,7 +139,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -159,7 +159,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender @Autowired constructor( private val customerPreferenceDao: CustomerPreferenceDao) { @@ -186,6 +186,38 @@ implementation type, consider declaring the most specific return type on your fa method (at least as specific as required by the injection points referring to your bean). ==== +.[[beans-autowired-annotation-self-injection]]Self Injection +**** +`@Autowired` also considers self references for injection (that is, references back to +the bean that is currently injected). + +Note, however, that self injection is a fallback mechanism. Regular dependencies on other +components always have precedence. In that sense, self references do not participate in +regular autowiring candidate selection and are therefore in particular never primary. On +the contrary, they always end up as lowest precedence. + +In practice, you should use self references as a last resort only – for example, for +calling other methods on the same instance through the bean's transactional proxy. As an +alternative, consider factoring out the affected methods to a separate delegate bean in +such a scenario. + +Another alternative is to use `@Resource`, which may obtain a proxy back to the current +bean by its unique name. + +====== +[NOTE] +==== +Trying to inject the results from `@Bean` methods in the same `@Configuration` class is +effectively a self-reference scenario as well. Either lazily resolve such references +in the method signature where it is actually needed (as opposed to an autowired field +in the configuration class) or declare the affected `@Bean` methods as `static`, +decoupling them from the containing configuration class instance and its lifecycle. +Otherwise, such beans are only considered in the fallback phase, with matching beans +on other configuration classes selected as primary candidates instead (if available). +==== +====== +**** + You can also instruct Spring to provide all beans of a particular type from the `ApplicationContext` by adding the `@Autowired` annotation to a field or method that expects an array of that type, as the following example shows: @@ -194,7 +226,7 @@ expects an array of that type, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -207,7 +239,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -225,7 +257,7 @@ The same applies for typed collections, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -242,7 +274,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -268,6 +300,12 @@ use the same bean class). `@Order` values may influence priorities at injection but be aware that they do not influence singleton startup order, which is an orthogonal concern determined by dependency relationships and `@DependsOn` declarations. +Note that `@Order` annotations on configuration classes just influence the evaluation +order within the overall set of configuration classes on startup. Such configuration-level +order values do not affect the contained `@Bean` methods at all. For bean-level ordering, +each `@Bean` method needs to have its own `@Order` annotation which applies within a +set of multiple matches for the specific bean type (as returned by the factory method). + Note that the standard `jakarta.annotation.Priority` annotation is not available at the `@Bean` level, since it cannot be declared on methods. Its semantics can be modeled through `@Order` values in combination with `@Primary` on a single bean for each type. @@ -281,7 +319,7 @@ corresponding bean names, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -298,7 +336,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -323,7 +361,7 @@ non-required (i.e., by setting the `required` attribute in `@Autowired` to `fals ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleMovieLister { @@ -340,7 +378,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -406,15 +444,15 @@ through Java 8's `java.util.Optional`, as the following example shows: } ---- -As of Spring Framework 5.0, you can also use a `@Nullable` annotation (of any kind -in any package -- for example, `javax.annotation.Nullable` from JSR-305) or just leverage -Kotlin built-in null-safety support: +You can also use a parameter-level `@Nullable` annotation (of any kind in any package -- +for example, `javax.annotation.Nullable` from JSR-305) or just leverage Kotlin built-in +null-safety support: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleMovieLister { @@ -427,7 +465,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -439,6 +477,13 @@ Kotlin:: ---- ====== +[NOTE] +==== +A type-level `@Nullable` annotation such as from JSpecify is not supported in Spring +Framework 6.2 yet. You need to upgrade to Spring Framework 7.0 where the framework +detects type-level annotations and consistently declares JSpecify in its own codebase. +==== + You can also use `@Autowired` for interfaces that are well-known resolvable dependencies: `BeanFactory`, `ApplicationContext`, `Environment`, `ResourceLoader`, `ApplicationEventPublisher`, and `MessageSource`. These interfaces and their extended @@ -450,7 +495,7 @@ an `ApplicationContext` object: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -466,7 +511,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/custom-autowire-configurer.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/custom-autowire-configurer.adoc index 34d63d008483..0ca89cd0ab46 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/custom-autowire-configurer.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/custom-autowire-configurer.adoc @@ -1,7 +1,7 @@ [[beans-custom-autowire-configurer]] = Using `CustomAutowireConfigurer` -{api-spring-framework}/beans/factory/annotation/CustomAutowireConfigurer.html[`CustomAutowireConfigurer`] +{spring-framework-api}/beans/factory/annotation/CustomAutowireConfigurer.html[`CustomAutowireConfigurer`] is a `BeanFactoryPostProcessor` that lets you register your own custom qualifier annotation types, even if they are not annotated with Spring's `@Qualifier` annotation. The following example shows how to use `CustomAutowireConfigurer`: diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/generics-as-qualifiers.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/generics-as-qualifiers.adoc index f4dac3a0461b..2182e114d79e 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/generics-as-qualifiers.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/generics-as-qualifiers.adoc @@ -9,7 +9,7 @@ configuration: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class MyConfiguration { @@ -28,7 +28,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class MyConfiguration { @@ -50,7 +50,7 @@ used as a qualifier, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Autowired private Store s1; // qualifier, injects the stringStore bean @@ -61,7 +61,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Autowired private lateinit var s1: Store // qualifier, injects the stringStore bean @@ -78,7 +78,7 @@ following example autowires a generic `List`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Inject all Store beans as long as they have an generic // Store beans will not appear in this list @@ -88,7 +88,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Inject all Store beans as long as they have an generic // Store beans will not appear in this list diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/postconstruct-and-predestroy-annotations.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/postconstruct-and-predestroy-annotations.adoc index 4c9a1bdcbf3a..12afd6d4aff4 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/postconstruct-and-predestroy-annotations.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/postconstruct-and-predestroy-annotations.adoc @@ -17,7 +17,7 @@ cleared upon destruction: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class CachingMovieLister { @@ -35,7 +35,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CachingMovieLister { diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/resource.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/resource.adoc index b3457b78833b..0c24fd143ae9 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/resource.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/resource.adoc @@ -15,7 +15,7 @@ as demonstrated in the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleMovieLister { @@ -31,7 +31,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -54,7 +54,7 @@ named `movieFinder` injected into its setter method: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleMovieLister { @@ -69,7 +69,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -84,7 +84,7 @@ Kotlin:: NOTE: The name provided with the annotation is resolved as a bean name by the `ApplicationContext` of which the `CommonAnnotationBeanPostProcessor` is aware. The names can be resolved through JNDI if you configure Spring's -{api-spring-framework}/jndi/support/SimpleJndiBeanFactory.html[`SimpleJndiBeanFactory`] +{spring-framework-api}/jndi/support/SimpleJndiBeanFactory.html[`SimpleJndiBeanFactory`] explicitly. However, we recommend that you rely on the default behavior and use Spring's JNDI lookup capabilities to preserve the level of indirection. @@ -103,7 +103,7 @@ named "customerPreferenceDao" and then falls back to a primary type match for th ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -124,7 +124,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { 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 967e04af4e70..13f20afe733d 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 @@ -7,7 +7,7 @@ ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class MovieRecommender { @@ -22,7 +22,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class MovieRecommender(@Value("\${catalog.name}") private val catalog: String) @@ -35,7 +35,7 @@ With the following configuration: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @PropertySource("classpath:application.properties") @@ -44,7 +44,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @PropertySource("classpath:application.properties") @@ -71,7 +71,7 @@ example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -85,7 +85,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -101,8 +101,8 @@ 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`, or `setValueSeparator` to customize -placeholders. +`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or +`setEscapeCharacter` to customize placeholders. NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that will get properties from `application.properties` and `application.yml` files. @@ -117,7 +117,7 @@ It is possible to provide a default value as following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class MovieRecommender { @@ -132,7 +132,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class MovieRecommender(@Value("\${catalog.name:defaultCatalog}") private val catalog: String) @@ -148,7 +148,7 @@ provide conversion support for your own custom type, you can provide your own ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -164,7 +164,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -186,7 +186,7 @@ computed at runtime as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class MovieRecommender { @@ -201,7 +201,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class MovieRecommender( @@ -215,7 +215,7 @@ SpEL also enables the use of more complex data structures: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class MovieRecommender { @@ -231,7 +231,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class MovieRecommender( diff --git a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc index 57e51a6f1e9f..ab6562f740f7 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc @@ -2,36 +2,28 @@ = Container Overview The `org.springframework.context.ApplicationContext` interface represents the Spring IoC -container and is responsible for instantiating, configuring, and assembling the -beans. The container gets its instructions on what objects to -instantiate, configure, and assemble by reading configuration metadata. The -configuration metadata is represented in XML, Java annotations, or Java code. It lets -you express the objects that compose your application and the rich interdependencies -between those objects. - -Several implementations of the `ApplicationContext` interface are supplied -with Spring. In stand-alone applications, it is common to create an -instance of -{api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`] -or {api-spring-framework}/context/support/FileSystemXmlApplicationContext.html[`FileSystemXmlApplicationContext`]. -While XML has been the traditional format for defining configuration metadata, you can -instruct the container to use Java annotations or code as the metadata format by -providing a small amount of XML configuration to declaratively enable support for these -additional metadata formats. +container and is responsible for instantiating, configuring, and assembling the beans. +The container gets its instructions on the components to instantiate, configure, and +assemble by reading configuration metadata. The configuration metadata can be represented +as annotated component classes, configuration classes with factory methods, or external +XML files or Groovy scripts. With either format, you may compose your application and the +rich interdependencies between those components. + +Several implementations of the `ApplicationContext` interface are part of core Spring. +In stand-alone applications, it is common to create an instance of +{spring-framework-api}/context/annotation/AnnotationConfigApplicationContext.html[`AnnotationConfigApplicationContext`] +or {spring-framework-api}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`]. In most application scenarios, explicit user code is not required to instantiate one or -more instances of a Spring IoC container. For example, in a web application scenario, a -simple eight (or so) lines of boilerplate web descriptor XML in the `web.xml` file -of the application typically suffices (see +more instances of a Spring IoC container. For example, in a plain web application scenario, +a simple boilerplate web descriptor XML in the `web.xml` file of the application suffices (see xref:core/beans/context-introduction.adoc#context-create[Convenient ApplicationContext Instantiation for Web Applications]). -If you use the https://spring.io/tools[Spring Tools for Eclipse] (an Eclipse-powered -development environment), you can easily create this boilerplate configuration with a -few mouse clicks or keystrokes. +In a Spring Boot scenario, the application context is implicitly bootstrapped for you +based on common setup conventions. The following diagram shows a high-level view of how Spring works. Your application classes are combined with configuration metadata so that, after the `ApplicationContext` is -created and initialized, you have a fully configured and executable system or -application. +created and initialized, you have a fully configured and executable system or application. .The Spring IoC container image::container-magic.png[] @@ -43,33 +35,25 @@ image::container-magic.png[] As the preceding diagram shows, the Spring IoC container consumes a form of configuration metadata. This configuration metadata represents how you, as an -application developer, tell the Spring container to instantiate, configure, and assemble -the objects in your application. +application developer, tell the Spring container to instantiate, configure, +and assemble the components in your application. -Configuration metadata is traditionally supplied in a simple and intuitive XML format, -which is what most of this chapter uses to convey key concepts and features of the -Spring IoC container. - -NOTE: XML-based metadata is not the only allowed form of configuration metadata. The Spring IoC container itself is totally decoupled from the format in which this configuration metadata is actually written. These days, many developers choose -xref:core/beans/java.adoc[Java-based configuration] for their Spring applications. - -For information about using other forms of metadata with the Spring container, see: +xref:core/beans/java.adoc[Java-based configuration] for their Spring applications: * xref:core/beans/annotation-config.adoc[Annotation-based configuration]: define beans using - annotation-based configuration metadata. + annotation-based configuration metadata on your application's component classes. * xref:core/beans/java.adoc[Java-based configuration]: define beans external to your application - classes by using Java rather than XML files. To use these features, see the - {api-spring-framework}/context/annotation/Configuration.html[`@Configuration`], - {api-spring-framework}/context/annotation/Bean.html[`@Bean`], - {api-spring-framework}/context/annotation/Import.html[`@Import`], - and {api-spring-framework}/context/annotation/DependsOn.html[`@DependsOn`] annotations. + classes by using Java-based configuration classes. To use these features, see the + {spring-framework-api}/context/annotation/Configuration.html[`@Configuration`], + {spring-framework-api}/context/annotation/Bean.html[`@Bean`], + {spring-framework-api}/context/annotation/Import.html[`@Import`], + and {spring-framework-api}/context/annotation/DependsOn.html[`@DependsOn`] annotations. -Spring configuration consists of at least one and typically more than one bean -definition that the container must manage. XML-based configuration metadata configures these -beans as `` elements inside a top-level `` element. Java -configuration typically uses `@Bean`-annotated methods within a `@Configuration` class. +Spring configuration consists of at least one and typically more than one bean definition +that the container must manage. Java configuration typically uses `@Bean`-annotated +methods within a `@Configuration` class, each corresponding to one bean definition. These bean definitions correspond to the actual objects that make up your application. Typically, you define service layer objects, persistence layer objects such as @@ -79,7 +63,14 @@ Typically, one does not configure fine-grained domain objects in the container, it is usually the responsibility of repositories and business logic to create and load domain objects. -The following example shows the basic structure of XML-based configuration metadata: + + +[[beans-factory-xml]] +=== XML as an External Configuration DSL + +XML-based configuration metadata configures these beans as `` elements inside +a top-level `` element. The following example shows the basic structure of +XML-based configuration metadata: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -110,28 +101,23 @@ The value of the `id` attribute can be used to refer to collaborating objects. T for referring to collaborating objects is not shown in this example. See xref:core/beans/dependencies.adoc[Dependencies] for more information. - - -[[beans-factory-instantiation]] -== Instantiating a Container - -The location path or paths -supplied to an `ApplicationContext` constructor are resource strings that let -the container load configuration metadata from a variety of external resources, such +For instantiating a container, the location path or paths to the XML resource files +need to be supplied to a `ClassPathXmlApplicationContext` constructor that let the +container load configuration metadata from a variety of external resources, such as the local file system, the Java `CLASSPATH`, and so on. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") ---- @@ -141,7 +127,7 @@ Kotlin:: ==== After you learn about Spring's IoC container, you may want to know more about Spring's `Resource` abstraction (as described in -xref:web/webflux-webclient/client-builder.adoc#webflux-client-builder-reactor-resources[Resources]) +xref:core/resources.adoc[Resources]) which provides a convenient mechanism for reading an InputStream from locations defined in a URI syntax. In particular, `Resource` paths are used to construct applications contexts, as described in xref:core/resources.adoc#resources-app-ctx[Application Contexts and Resource Paths]. @@ -209,9 +195,9 @@ xref:core/beans/dependencies.adoc[Dependencies]. It can be useful to have bean definitions span multiple XML files. Often, each individual XML configuration file represents a logical layer or module in your architecture. -You can use the application context constructor to load bean definitions from all these +You can use the `ClassPathXmlApplicationContext` constructor to load bean definitions from XML fragments. This constructor takes multiple `Resource` locations, as was shown in the -xref:core/beans/basics.adoc#beans-factory-instantiation[previous section]. Alternatively, +xref:core/beans/basics.adoc#beans-factory-xml[previous section]. Alternatively, use one or more occurrences of the `` element to load bean definitions from another file or files. The following example shows how to do so: @@ -220,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 @@ -259,7 +244,7 @@ configuration features beyond plain bean definitions are available in a selectio of XML namespaces provided by Spring -- for example, the `context` and `util` namespaces. -[[groovy-bean-definition-dsl]] +[[beans-factory-groovy]] === The Groovy Bean Definition DSL As a further example for externalized configuration metadata, bean definitions can also @@ -308,7 +293,7 @@ example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // create and configure beans ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); @@ -322,7 +307,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -345,14 +330,14 @@ The following example shows Groovy configuration: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val context = GenericGroovyApplicationContext("services.groovy", "daos.groovy") ---- @@ -366,7 +351,7 @@ example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- GenericApplicationContext context = new GenericApplicationContext(); new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml"); @@ -375,7 +360,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val context = GenericApplicationContext() XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml") @@ -390,7 +375,7 @@ example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- GenericApplicationContext context = new GenericApplicationContext(); new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy"); @@ -399,7 +384,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val context = GenericApplicationContext() GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy") @@ -420,4 +405,3 @@ a dependency on a specific bean through metadata (such as an autowiring annotati - diff --git a/framework-docs/modules/ROOT/pages/core/beans/beanfactory.adoc b/framework-docs/modules/ROOT/pages/core/beans/beanfactory.adoc index 32837957748c..42d873fb82d2 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/beanfactory.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/beanfactory.adoc @@ -90,7 +90,7 @@ you need to programmatically call `addBeanPostProcessor`, as the following examp ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); // populate the factory with bean definitions @@ -104,7 +104,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val factory = DefaultListableBeanFactory() // populate the factory with bean definitions @@ -124,7 +124,7 @@ you need to call its `postProcessBeanFactory` method, as the following example s ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory); @@ -140,7 +140,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val factory = DefaultListableBeanFactory() val reader = XmlBeanDefinitionReader(factory) 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 c872157a60c9..611009b73f49 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc @@ -59,7 +59,7 @@ is meta-annotated with `@Component`, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -74,7 +74,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @@ -103,7 +103,7 @@ customization of the `proxyMode`. The following listing shows the definition of ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @@ -123,7 +123,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) @@ -142,7 +142,7 @@ You can then use `@SessionScope` without declaring the `proxyMode` as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Service @SessionScope @@ -153,7 +153,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Service @SessionScope @@ -169,7 +169,7 @@ You can also override the value for the `proxyMode`, as the following example sh ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Service @SessionScope(proxyMode = ScopedProxyMode.INTERFACES) @@ -180,7 +180,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Service @SessionScope(proxyMode = ScopedProxyMode.INTERFACES) @@ -191,7 +191,7 @@ Kotlin:: ====== For further details, see the -https://github.com/spring-projects/spring-framework/wiki/Spring-Annotation-Programming-Model[Spring Annotation Programming Model] +{spring-framework-wiki}/Spring-Annotation-Programming-Model[Spring Annotation Programming Model] wiki page. @@ -207,7 +207,7 @@ are eligible for such autodetection: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Service public class SimpleMovieLister { @@ -222,7 +222,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Service class SimpleMovieLister(private val movieFinder: MovieFinder) @@ -233,7 +233,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Repository public class JpaMovieFinder implements MovieFinder { @@ -243,7 +243,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repository class JpaMovieFinder : MovieFinder { @@ -262,7 +262,7 @@ comma- or semicolon- or space-separated list that includes the parent package of ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = "org.example") @@ -273,7 +273,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = ["org.example"]) @@ -315,10 +315,10 @@ entries in the classpath. When you build JARs with Ant, make sure that you do no activate the files-only switch of the JAR task. Also, classpath directories may not be exposed based on security policies in some environments -- for example, standalone apps on JDK 1.7.0_45 and higher (which requires 'Trusted-Library' setup in your manifests -- see -https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources). +{stackoverflow-questions}/19394570/java-jre-7u45-breaks-classloader-getresources). -On JDK 9's module path (Jigsaw), Spring's classpath scanning generally works as expected. -However, make sure that your component classes are exported in your `module-info` +On the module path (Java Module System), Spring's classpath scanning generally works as +expected. However, make sure that your component classes are exported in your `module-info` descriptors. If you expect Spring to invoke non-public members of your classes, make sure that they are 'opened' (that is, that they use an `opens` declaration instead of an `exports` declaration in your `module-info` descriptor). @@ -380,7 +380,7 @@ and using "`stub`" repositories instead: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = "org.example", @@ -393,7 +393,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = ["org.example"], @@ -438,7 +438,7 @@ annotated classes. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class FactoryMethodComponent { @@ -457,7 +457,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class FactoryMethodComponent { @@ -480,11 +480,15 @@ factory method and other bean definition properties, such as a qualifier value t the `@Qualifier` annotation. Other method-level annotations that can be specified are `@Scope`, `@Lazy`, and custom qualifier annotations. -TIP: In addition to its role for component initialization, you can also place the `@Lazy` +[[beans-factorybeans-annotations-lazy-injection-points]] +[TIP] +==== +In addition to its role for component initialization, you can also place the `@Lazy` annotation on injection points marked with `@Autowired` or `@Inject`. In this context, it leads to the injection of a lazy-resolution proxy. However, such a proxy approach is rather limited. For sophisticated lazy interactions, in particular in combination with optional dependencies, we recommend `ObjectProvider` instead. +==== Autowired fields and methods are supported, as previously discussed, with additional support for autowiring of `@Bean` methods. The following example shows how to do so: @@ -493,7 +497,7 @@ support for autowiring of `@Bean` methods. The following example shows how to do ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class FactoryMethodComponent { @@ -532,7 +536,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class FactoryMethodComponent { @@ -585,7 +589,7 @@ The following example shows how to use `InjectionPoint`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class FactoryMethodComponent { @@ -599,7 +603,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class FactoryMethodComponent { @@ -670,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 @@ -699,7 +701,7 @@ following component classes were detected, the names would be `myMovieLister` an ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Service("myMovieLister") public class SimpleMovieLister { @@ -709,7 +711,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Service("myMovieLister") class SimpleMovieLister { @@ -722,7 +724,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Repository public class MovieFinderImpl implements MovieFinder { @@ -732,7 +734,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repository class MovieFinderImpl : MovieFinder { @@ -743,7 +745,7 @@ Kotlin:: If you do not want to rely on the default bean-naming strategy, you can provide a custom bean-naming strategy. First, implement the -{api-spring-framework}/beans/factory/support/BeanNameGenerator.html[`BeanNameGenerator`] +{spring-framework-api}/beans/factory/support/BeanNameGenerator.html[`BeanNameGenerator`] interface, and be sure to include a default no-arg constructor. Then, provide the fully qualified class name when configuring the scanner, as the following example annotation and bean definition show. @@ -751,7 +753,7 @@ and bean definition show. TIP: If you run into naming conflicts due to multiple autodetected components having the same non-qualified class name (i.e., classes with identical names but residing in different packages), you may need to configure a `BeanNameGenerator` that defaults to the -fully qualified class name for the generated bean name. As of Spring Framework 5.2.3, the +fully qualified class name for the generated bean name. The `FullyQualifiedAnnotationBeanNameGenerator` located in package `org.springframework.context.annotation` can be used for such purposes. @@ -759,7 +761,7 @@ fully qualified class name for the generated bean name. As of Spring Framework 5 ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class) @@ -770,7 +772,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = ["org.example"], nameGenerator = MyNameGenerator::class) @@ -806,7 +808,7 @@ scope within the annotation, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Scope("prototype") @Repository @@ -817,7 +819,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Scope("prototype") @Repository @@ -840,7 +842,7 @@ possibly also declaring a custom scoped-proxy mode. NOTE: To provide a custom strategy for scope resolution rather than relying on the annotation-based approach, you can implement the -{api-spring-framework}/context/annotation/ScopeMetadataResolver.html[`ScopeMetadataResolver`] +{spring-framework-api}/context/annotation/ScopeMetadataResolver.html[`ScopeMetadataResolver`] interface. Be sure to include a default no-arg constructor. Then you can provide the fully qualified class name when configuring the scanner, as the following example of both an annotation and a bean definition shows: @@ -849,7 +851,7 @@ an annotation and a bean definition shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class) @@ -860,7 +862,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = ["org.example"], scopeResolver = MyScopeResolver::class) @@ -887,7 +889,7 @@ the following configuration results in standard JDK dynamic proxies: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES) @@ -898,7 +900,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = ["org.example"], scopedProxy = ScopedProxyMode.INTERFACES) @@ -920,7 +922,8 @@ Kotlin:: [[beans-scanning-qualifiers]] == Providing Qualifier Metadata with Annotations -The `@Qualifier` annotation is discussed in xref:core/beans/annotation-config/autowired-qualifiers.adoc[Fine-tuning Annotation-based Autowiring with Qualifiers]. +The `@Qualifier` annotation is discussed in +xref:core/beans/annotation-config/autowired-qualifiers.adoc[Fine-tuning Annotation-based Autowiring with Qualifiers]. The examples in that section demonstrate the use of the `@Qualifier` annotation and custom qualifier annotations to provide fine-grained control when you resolve autowire candidates. Because those examples were based on XML bean definitions, the qualifier @@ -934,7 +937,7 @@ technique: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component @Qualifier("Action") @@ -945,7 +948,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component @Qualifier("Action") @@ -957,7 +960,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component @Genre("Action") @@ -968,7 +971,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component @Genre("Action") @@ -982,7 +985,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component @Offline @@ -993,7 +996,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component @Offline 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 5fbc77c42b98..612185813e2f 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc @@ -1,10 +1,10 @@ [[context-introduction]] = Additional Capabilities of the `ApplicationContext` -As discussed in the xref:web/webmvc-view/mvc-xslt.adoc#mvc-view-xslt-beandefs[chapter introduction], the `org.springframework.beans.factory` +As discussed in the xref:core/beans/introduction.adoc[chapter introduction], the `org.springframework.beans.factory` package provides basic functionality for managing and manipulating beans, including in a programmatic way. The `org.springframework.context` package adds the -{api-spring-framework}/context/ApplicationContext.html[`ApplicationContext`] +{spring-framework-api}/context/ApplicationContext.html[`ApplicationContext`] interface, which extends the `BeanFactory` interface, in addition to extending other interfaces to provide additional functionality in a more application framework-oriented style. Many people use the `ApplicationContext` in a completely @@ -102,7 +102,7 @@ implementations and so can be cast to the `MessageSource` interface. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static void main(String[] args) { MessageSource resources = new ClassPathXmlApplicationContext("beans.xml"); @@ -113,7 +113,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun main() { val resources = ClassPathXmlApplicationContext("beans.xml") @@ -161,7 +161,7 @@ converted into `String` objects and inserted into placeholders in the lookup mes ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class Example { @@ -181,7 +181,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Example { @@ -224,7 +224,7 @@ argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static void main(final String[] args) { MessageSource resources = new ClassPathXmlApplicationContext("beans.xml"); @@ -236,7 +236,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun main() { val resources = ClassPathXmlApplicationContext("beans.xml") @@ -269,7 +269,7 @@ file format but is more flexible than the standard JDK based `ResourceBundleMessageSource` implementation. In particular, it allows for reading files from any Spring resource location (not only from the classpath) and supports hot reloading of bundle property files (while efficiently caching them in between). -See the {api-spring-framework}/context/support/ReloadableResourceBundleMessageSource.html[`ReloadableResourceBundleMessageSource`] +See the {spring-framework-api}/context/support/ReloadableResourceBundleMessageSource.html[`ReloadableResourceBundleMessageSource`] javadoc for details. @@ -344,7 +344,7 @@ simple class that extends Spring's `ApplicationEvent` base class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class BlockedListEvent extends ApplicationEvent { @@ -363,7 +363,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class BlockedListEvent(source: Any, val address: String, @@ -380,7 +380,7 @@ example shows such a class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class EmailService implements ApplicationEventPublisherAware { @@ -407,7 +407,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class EmailService : ApplicationEventPublisherAware { @@ -447,7 +447,7 @@ shows such a class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class BlockedListNotifier implements ApplicationListener { @@ -465,7 +465,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class BlockedListNotifier : ApplicationListener { @@ -484,9 +484,9 @@ You can register as many event listeners as you wish, but note that, by default, This means that the `publishEvent()` method blocks until all listeners have finished processing the event. One advantage of this synchronous and single-threaded approach is that, when a listener receives an event, it operates inside the transaction context of the publisher if a transaction context is available. -If another strategy for event publication becomes necessary, e.g. asynchronous event processing by default, -see the javadoc for Spring's {api-spring-framework}/context/event/ApplicationEventMulticaster.html[`ApplicationEventMulticaster`] interface -and {api-spring-framework}/context/event/SimpleApplicationEventMulticaster.html[`SimpleApplicationEventMulticaster`] implementation +If another strategy for event publication becomes necessary, for example, asynchronous event processing by default, +see the javadoc for Spring's {spring-framework-api}/context/event/ApplicationEventMulticaster.html[`ApplicationEventMulticaster`] interface +and {spring-framework-api}/context/event/SimpleApplicationEventMulticaster.html[`SimpleApplicationEventMulticaster`] implementation for configuration options which can be applied to a custom "applicationEventMulticaster" bean definition. In these cases, ThreadLocals and logging context are not propagated for the event processing. See xref:integration/observability.adoc#observability.application-events[the `@EventListener` Observability section] @@ -529,7 +529,7 @@ notify appropriate parties. NOTE: Spring's eventing mechanism is designed for simple communication between Spring beans within the same application context. However, for more sophisticated enterprise integration needs, the separately maintained -https://projects.spring.io/spring-integration/[Spring Integration] project provides +{spring-site-projects}/spring-integration/[Spring Integration] project provides complete support for building lightweight, https://www.enterpriseintegrationpatterns.com[pattern-oriented], event-driven architectures that build upon the well-known Spring programming model. @@ -545,7 +545,7 @@ You can register an event listener on any method of a managed bean by using the ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class BlockedListNotifier { @@ -564,7 +564,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class BlockedListNotifier { @@ -578,6 +578,8 @@ Kotlin:: ---- ====== +NOTE: Do not define such beans to be lazy as the `ApplicationContext` will honour that and will not register the method to listen to events. + The method signature once again declares the event type to which it listens, but, this time, with a flexible name and without implementing a specific listener interface. The event type can also be narrowed through generics as long as the actual event type @@ -591,7 +593,7 @@ following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class}) public void handleContextStart() { @@ -601,7 +603,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @EventListener(ContextStartedEvent::class, ContextRefreshedEvent::class) fun handleContextStart() { @@ -621,7 +623,7 @@ The following example shows how our notifier can be rewritten to be invoked only ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @EventListener(condition = "#blEvent.content == 'my-event'") public void processBlockedListEvent(BlockedListEvent blEvent) { @@ -631,7 +633,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @EventListener(condition = "#blEvent.content == 'my-event'") fun processBlockedListEvent(blEvent: BlockedListEvent) { @@ -644,7 +646,7 @@ Each `SpEL` expression evaluates against a dedicated context. The following tabl items made available to the context so that you can use them for conditional event processing: [[context-functionality-events-annotation-tbl]] -.Event SpEL available metadata +.Event metadata available in SpEL expressions |=== | Name| Location| Description| Example @@ -660,8 +662,8 @@ items made available to the context so that you can use them for conditional eve | __Argument name__ | evaluation context -| The name of any of the method arguments. If, for some reason, the names are not available - (for example, because there is no debug information in the compiled byte code), individual +| The name of a particular method argument. If the names are not available + (for example, because the code was compiled without the `-parameters` flag), individual arguments are also available using the `#a<#arg>` syntax where `<#arg>` stands for the argument index (starting from 0). | `#blEvent` or `#a0` (you can also use `#p0` or `#p<#arg>` parameter notation as an alias) @@ -677,7 +679,7 @@ method signature to return the event that should be published, as the following ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @EventListener public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) { @@ -688,7 +690,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @EventListener fun handleBlockedListEvent(event: BlockedListEvent): ListUpdateEvent { @@ -717,7 +719,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @EventListener @Async @@ -728,7 +730,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @EventListener @Async @@ -742,11 +744,11 @@ Be aware of the following limitations when using asynchronous events: * If an asynchronous event listener throws an `Exception`, it is not propagated to the caller. See - {api-spring-framework}/aop/interceptor/AsyncUncaughtExceptionHandler.html[`AsyncUncaughtExceptionHandler`] + {spring-framework-api}/aop/interceptor/AsyncUncaughtExceptionHandler.html[`AsyncUncaughtExceptionHandler`] for more details. * Asynchronous event listener methods cannot publish a subsequent event by returning a value. If you need to publish another event as the result of the processing, inject an - {api-spring-framework}/context/ApplicationEventPublisher.html[`ApplicationEventPublisher`] + {spring-framework-api}/context/ApplicationEventPublisher.html[`ApplicationEventPublisher`] to publish the event manually. * ThreadLocals and logging context are not propagated by default for the event processing. See xref:integration/observability.adoc#observability.application-events[the `@EventListener` Observability section] @@ -763,7 +765,7 @@ annotation to the method declaration, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @EventListener @Order(42) @@ -774,7 +776,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @EventListener @Order(42) @@ -797,7 +799,7 @@ can create the following listener definition to receive only `EntityCreatedEvent ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @EventListener public void onPersonCreated(EntityCreatedEvent event) { @@ -807,7 +809,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @EventListener fun onPersonCreated(event: EntityCreatedEvent) { @@ -829,7 +831,7 @@ environment provides. The following event shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class EntityCreatedEvent extends ApplicationEvent implements ResolvableTypeProvider { @@ -846,7 +848,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class EntityCreatedEvent(entity: T) : ApplicationEvent(entity), ResolvableTypeProvider { @@ -864,7 +866,7 @@ Finally, as with classic `ApplicationListener` implementations, the actual multi happens via a context-wide `ApplicationEventMulticaster` at runtime. By default, this is a `SimpleApplicationEventMulticaster` with synchronous event publication in the caller thread. This can be replaced/customized through an "applicationEventMulticaster" bean definition, -e.g. for processing all events asynchronously and/or for handling listener exceptions: +for example, for processing all events asynchronously and/or for handling listener exceptions: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -883,11 +885,12 @@ e.g. for processing all events asynchronously and/or for handling listener excep == Convenient Access to Low-level Resources For optimal usage and understanding of application contexts, you should familiarize -yourself with Spring's `Resource` abstraction, as described in xref:web/webflux-webclient/client-builder.adoc#webflux-client-builder-reactor-resources[Resources]. +yourself with Spring's `Resource` abstraction, as described in +xref:core/resources.adoc[Resources]. An application context is a `ResourceLoader`, which can be used to load `Resource` objects. A `Resource` is essentially a more feature rich version of the JDK `java.net.URL` class. -In fact, the implementations of the `Resource` wrap an instance of `java.net.URL`, where +In fact, implementations of `Resource` wrap an instance of `java.net.URL`, where appropriate. A `Resource` can obtain low-level resources from almost any location in a transparent fashion, including from the classpath, a filesystem location, anywhere describable with a standard URL, and some other variations. If the resource location @@ -935,7 +938,7 @@ Here is an example of instrumentation in the `AnnotationConfigApplicationContext ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // create a startup step and start recording StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan"); @@ -949,7 +952,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // create a startup step and start recording val scanPackages = getApplicationStartup().start("spring.context.base-packages.scan") @@ -1040,7 +1043,7 @@ and JMX support facilities. Application components can also interact with the ap server's JCA `WorkManager` through Spring's `TaskExecutor` abstraction. See the javadoc of the -{api-spring-framework}/jca/context/SpringContextResourceAdapter.html[`SpringContextResourceAdapter`] +{spring-framework-api}/jca/context/SpringContextResourceAdapter.html[`SpringContextResourceAdapter`] class for the configuration details involved in RAR deployment. For a simple deployment of a Spring ApplicationContext as a Jakarta EE RAR file: @@ -1050,7 +1053,7 @@ all application classes into a RAR file (which is a standard JAR file with a dif file extension). . Add all required library JARs into the root of the RAR archive. . Add a -`META-INF/ra.xml` deployment descriptor (as shown in the {api-spring-framework}/jca/context/SpringContextResourceAdapter.html[javadoc for `SpringContextResourceAdapter`]) +`META-INF/ra.xml` deployment descriptor (as shown in the {spring-framework-api}/jca/context/SpringContextResourceAdapter.html[javadoc for `SpringContextResourceAdapter`]) and the corresponding Spring XML bean definition file(s) (typically `META-INF/applicationContext.xml`). . Drop the resulting RAR file into your diff --git a/framework-docs/modules/ROOT/pages/core/beans/context-load-time-weaver.adoc b/framework-docs/modules/ROOT/pages/core/beans/context-load-time-weaver.adoc index 73579b414ded..3857be9891e8 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/context-load-time-weaver.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/context-load-time-weaver.adoc @@ -11,7 +11,7 @@ To enable load-time weaving, you can add the `@EnableLoadTimeWeaving` to one of ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableLoadTimeWeaving @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableLoadTimeWeaving @@ -44,7 +44,7 @@ weaver instance. This is particularly useful in combination with xref:data-access/orm/jpa.adoc[Spring's JPA support] where load-time weaving may be necessary for JPA class transformation. Consult the -{api-spring-framework}/orm/jpa/LocalContainerEntityManagerFactoryBean.html[`LocalContainerEntityManagerFactoryBean`] +{spring-framework-api}/orm/jpa/LocalContainerEntityManagerFactoryBean.html[`LocalContainerEntityManagerFactoryBean`] javadoc for more detail. For more on AspectJ load-time weaving, see xref:core/aop/using-aspectj.adoc#aop-aj-ltw[Load-time Weaving with AspectJ in the Spring Framework]. diff --git a/framework-docs/modules/ROOT/pages/core/beans/definition.adoc b/framework-docs/modules/ROOT/pages/core/beans/definition.adoc index 2141abd706ff..4be43e5371c9 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/definition.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/definition.adoc @@ -57,9 +57,9 @@ The following table describes these properties: In addition to bean definitions that contain information on how to create a specific bean, the `ApplicationContext` implementations also permit the registration of existing objects that are created outside the container (by users). This is done by accessing the -ApplicationContext's `BeanFactory` through the `getBeanFactory()` method, which returns -the `DefaultListableBeanFactory` implementation. `DefaultListableBeanFactory` supports -this registration through the `registerSingleton(..)` and `registerBeanDefinition(..)` +ApplicationContext's `BeanFactory` through the `getAutowireCapableBeanFactory()` method, +which returns the `DefaultListableBeanFactory` implementation. `DefaultListableBeanFactory` +supports this registration through the `registerSingleton(..)` and `registerBeanDefinition(..)` methods. However, typical applications work solely with beans defined through regular bean definition metadata. @@ -75,6 +75,36 @@ lead to concurrent access exceptions, inconsistent state in the bean container, +[[beans-definition-overriding]] +== Overriding Beans + +Bean overriding occurs when a bean is registered using an identifier that is already +allocated. While bean overriding is possible, it makes the configuration harder to read. + +WARNING: Bean overriding will be deprecated in a future release. + +To disable bean overriding altogether, you can set the `allowBeanDefinitionOverriding` +flag to `false` on the `ApplicationContext` before it is refreshed. In such a setup, an +exception is thrown if bean overriding is used. + +By default, the container logs every attempt to override a bean at `INFO` level so that +you can adapt your configuration accordingly. While not recommended, you can silence +those logs by setting the `allowBeanDefinitionOverriding` flag to `true`. + +.Java Configuration +**** +If you use Java Configuration, a corresponding `@Bean` method always silently overrides +a scanned bean class with the same component name as long as the return type of the +`@Bean` method matches that bean class. This simply means that the container will call +the `@Bean` factory method in favor of any pre-declared constructor on the bean class. +**** + +NOTE: We acknowledge that overriding beans in test scenarios is convenient, and there is +explicit support for this as of Spring Framework 6.2. Please refer to +xref:testing/testcontext-framework/bean-overriding.adoc[this section] for more details. + + + [[beans-beanname]] == Naming Beans @@ -234,6 +264,10 @@ For details about the mechanism for supplying arguments to the constructor (if r and setting object instance properties after the object is constructed, see xref:core/beans/dependencies/factory-collaborators.adoc[Injecting Dependencies]. +NOTE: In the case of constructor arguments, the container can select a corresponding +constructor among several overloaded constructors. That said, to avoid ambiguities, +it is recommended to keep your constructor signatures as straightforward as possible. + [[beans-factory-class-static-factory-method]] === Instantiation with a Static Factory Method @@ -264,7 +298,7 @@ The following example shows a class that would work with the preceding bean defi ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ClientService { private static ClientService clientService = new ClientService(); @@ -278,7 +312,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ClientService private constructor() { companion object { @@ -294,6 +328,24 @@ For details about the mechanism for supplying (optional) arguments to the factor and setting object instance properties after the object is returned from the factory, see xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail]. +NOTE: In the case of factory method arguments, the container can select a corresponding +method among several overloaded methods of the same name. That said, to avoid ambiguities, +it is recommended to keep your factory method signatures as straightforward as possible. + +[TIP] +==== +A typical problematic case with factory method overloading is Mockito with its many +overloads of the `mock` method. Choose the most specific variant of `mock` possible: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + +---- +==== + [[beans-factory-class-instance-factory-method]] === Instantiation by Using an Instance Factory Method @@ -326,7 +378,7 @@ The following example shows the corresponding class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class DefaultServiceLocator { @@ -340,7 +392,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class DefaultServiceLocator { companion object { @@ -376,7 +428,7 @@ The following example shows the corresponding class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class DefaultServiceLocator { @@ -396,7 +448,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class DefaultServiceLocator { companion object { @@ -416,8 +468,8 @@ Kotlin:: ====== This approach shows that the factory bean itself can be managed and configured through -dependency injection (DI). See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail] -. +dependency injection (DI). +See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail]. NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the Spring container and that creates objects through an @@ -444,5 +496,3 @@ cases into account and returns the type of object that a `BeanFactory.getBean` c going to return for the same bean name. - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-autowire.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-autowire.adoc index 829fe815a81c..fa7ba3de4840 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-autowire.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-autowire.adoc @@ -60,6 +60,7 @@ instance's values consist of all bean instances that match the expected type, an `Map` instance's keys contain the corresponding bean names. + [[beans-autowired-exceptions]] == Limitations and Disadvantages of Autowiring @@ -101,10 +102,10 @@ In the latter scenario, you have several options: == Excluding a Bean from Autowiring On a per-bean basis, you can exclude a bean from autowiring. In Spring's XML format, set -the `autowire-candidate` attribute of the `` element to `false`. The container -makes that specific bean definition unavailable to the autowiring infrastructure -(including annotation style configurations such as xref:core/beans/annotation-config/autowired.adoc[`@Autowired`] -). +the `autowire-candidate` attribute of the `` element to `false`; with the `@Bean` +annotation, the attribute is named `autowireCandidate`. The container makes that specific +bean definition unavailable to the autowiring infrastructure, including annotation-based +injection points such as xref:core/beans/annotation-config/autowired.adoc[`@Autowired`]. NOTE: The `autowire-candidate` attribute is designed to only affect type-based autowiring. It does not affect explicit references by name, which get resolved even if the @@ -119,9 +120,25 @@ provide multiple patterns, define them in a comma-separated list. An explicit va `true` or `false` for a bean definition's `autowire-candidate` attribute always takes precedence. For such beans, the pattern matching rules do not apply. -These techniques are useful for beans that you never want to be injected into other -beans by autowiring. It does not mean that an excluded bean cannot itself be configured by +These techniques are useful for beans that you never want to be injected into other beans +by autowiring. It does not mean that an excluded bean cannot itself be configured by using autowiring. Rather, the bean itself is not a candidate for autowiring other beans. +[NOTE] +==== +As of 6.2, `@Bean` methods support two variants of the autowire candidate flag: +`autowireCandidate` and `defaultCandidate`. + +When using xref:core/beans/annotation-config/autowired-qualifiers.adoc[qualifiers], +a bean marked with `defaultCandidate=false` is only available for injection points +where an additional qualifier indication is present. This is useful for restricted +delegates that are supposed to be injectable in certain areas but are not meant to +get in the way of beans of the same type in other places. Such a bean will never +get injected by plain declared type only, rather by type plus specific qualifier. + +In contrast, `autowireCandidate=false` behaves exactly like the `autowire-candidate` +attribute as explained above: Such a bean will never get injected by type at all. +==== + diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-collaborators.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-collaborators.adoc index 25bbaff2b63b..2559135e4d92 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-collaborators.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-collaborators.adoc @@ -34,7 +34,7 @@ injection: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleMovieLister { @@ -52,7 +52,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // a constructor so that the Spring container can inject a MovieFinder class SimpleMovieLister(private val movieFinder: MovieFinder) { @@ -77,7 +77,7 @@ being instantiated. Consider the following class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package x.y; @@ -91,7 +91,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package x.y @@ -127,7 +127,7 @@ by type without help. Consider the following class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package examples; @@ -148,7 +148,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package examples @@ -159,10 +159,12 @@ Kotlin:: ---- ====== -.[[beans-factory-ctor-arguments-type]]Constructor argument type matching --- +[discrete] +[[beans-factory-ctor-arguments-type]] +==== Constructor argument type matching + In the preceding scenario, the container can use type matching with simple types if -you explicitly specify the type of the constructor argument by using the `type` attribute, +you explicitly specify the type of the constructor argument via the `type` attribute, as the following example shows: [source,xml,indent=0,subs="verbatim,quotes"] @@ -172,10 +174,11 @@ as the following example shows: ---- --- -.[[beans-factory-ctor-arguments-index]]Constructor argument index --- +[discrete] +[[beans-factory-ctor-arguments-index]] +==== Constructor argument index + You can use the `index` attribute to specify explicitly the index of constructor arguments, as the following example shows: @@ -191,10 +194,11 @@ In addition to resolving the ambiguity of multiple simple values, specifying an resolves ambiguity where a constructor has two arguments of the same type. NOTE: The index is 0-based. --- -.[[beans-factory-ctor-arguments-name]]Constructor argument name --- +[discrete] +[[beans-factory-ctor-arguments-name]] +==== Constructor argument name + You can also use the constructor parameter name for value disambiguation, as the following example shows: @@ -207,8 +211,8 @@ example shows: ---- Keep in mind that, to make this work out of the box, your code must be compiled with the -debug flag enabled so that Spring can look up the parameter name from the constructor. -If you cannot or do not want to compile your code with the debug flag, you can use the +`-parameters` flag enabled so that Spring can look up the parameter name from the constructor. +If you cannot or do not want to compile your code with the `-parameters` flag, you can use the https://download.oracle.com/javase/8/docs/api/java/beans/ConstructorProperties.html[@ConstructorProperties] JDK annotation to explicitly name your constructor arguments. The sample class would then have to look as follows: @@ -217,7 +221,7 @@ then have to look as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package examples; @@ -235,7 +239,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package examples @@ -244,7 +248,6 @@ Kotlin:: constructor(val years: Int, val ultimateAnswer: String) ---- ====== --- [[beans-setter-injection]] @@ -262,7 +265,7 @@ on container specific interfaces, base classes, or annotations. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleMovieLister { @@ -280,7 +283,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -437,7 +440,7 @@ The following example shows the corresponding `ExampleBean` class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ExampleBean { @@ -463,7 +466,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleBean { lateinit var beanOne: AnotherBean @@ -500,7 +503,7 @@ The following example shows the corresponding `ExampleBean` class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ExampleBean { @@ -521,7 +524,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleBean( private val beanOne: AnotherBean, @@ -554,7 +557,7 @@ The following example shows the corresponding `ExampleBean` class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ExampleBean { @@ -578,7 +581,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleBean private constructor() { companion object { diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-dependson.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-dependson.adoc index 17e5e98246bb..fdf50af50671 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-dependson.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-dependson.adoc @@ -2,13 +2,15 @@ = Using `depends-on` If a bean is a dependency of another bean, that usually means that one bean is set as a -property of another. Typically you accomplish this with the <` -element>> in XML-based configuration metadata. However, sometimes dependencies between -beans are less direct. An example is when a static initializer in a class needs to be -triggered, such as for database driver registration. The `depends-on` attribute can -explicitly force one or more beans to be initialized before the bean using this element -is initialized. The following example uses the `depends-on` attribute to express a -dependency on a single bean: +property of another. Typically you accomplish this with the +xref:core/beans/dependencies/factory-properties-detailed.adoc#beans-ref-element[`` element] +in XML-based metadata or through xref:core/beans/dependencies/factory-autowire.adoc[autowiring]. + +However, sometimes dependencies between beans are less direct. An example is when a static +initializer in a class needs to be triggered, such as for database driver registration. +The `depends-on` attribute or `@DependsOn` annotation can explicitly force one or more beans +to be initialized before the bean using this element is initialized. The following example +uses the `depends-on` attribute to express a dependency on a single bean: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -31,10 +33,10 @@ delimiters): ---- NOTE: The `depends-on` attribute can specify both an initialization-time dependency and, -in the case of xref:core/beans/factory-scopes.adoc#beans-factory-scopes-singleton[singleton] beans only, a corresponding -destruction-time dependency. Dependent beans that define a `depends-on` relationship -with a given bean are destroyed first, prior to the given bean itself being destroyed. -Thus, `depends-on` can also control shutdown order. +in the case of xref:core/beans/factory-scopes.adoc#beans-factory-scopes-singleton[singleton] +beans only, a corresponding destruction-time dependency. Dependent beans that define a +`depends-on` relationship with a given bean are destroyed first, prior to the given bean +itself being destroyed. Thus, `depends-on` can also control shutdown order. diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-lazy-init.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-lazy-init.adoc index 59fd3a319b9e..353cf0ae6e1e 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-lazy-init.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-lazy-init.adoc @@ -10,33 +10,25 @@ pre-instantiation of a singleton bean by marking the bean definition as being lazy-initialized. A lazy-initialized bean tells the IoC container to create a bean instance when it is first requested, rather than at startup. -In XML, this behavior is controlled by the `lazy-init` attribute on the `` -element, as the following example shows: +This behavior is controlled by the `@Lazy` annotation or in XML the `lazy-init` attribute on the `` element, as +the following example shows: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - ----- +include-code::./ApplicationConfiguration[tag=snippet,indent=0] When the preceding configuration is consumed by an `ApplicationContext`, the `lazy` bean is not eagerly pre-instantiated when the `ApplicationContext` starts, -whereas the `not.lazy` bean is eagerly pre-instantiated. +whereas the `notLazy` one is eagerly pre-instantiated. However, when a lazy-initialized bean is a dependency of a singleton bean that is not lazy-initialized, the `ApplicationContext` creates the lazy-initialized bean at startup, because it must satisfy the singleton's dependencies. The lazy-initialized bean is injected into a singleton bean elsewhere that is not lazy-initialized. -You can also control lazy-initialization at the container level by using the -`default-lazy-init` attribute on the `` element, as the following example shows: +You can also control lazy-initialization for a set of beans by using the `@Lazy` annotation on your `@Configuration` +annotated class or in XML using the `default-lazy-init` attribute on the `` element, as the following example +shows: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - ----- +include-code::./LazyConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc index 108202c40ac7..a8096c29972b 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-method-injection.adoc @@ -21,7 +21,7 @@ shows this approach: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages",fold="none"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages",fold="none"] ---- package fiona.apple; @@ -60,7 +60,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages",fold="none"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages",fold="none"] ---- package fiona.apple @@ -99,7 +99,7 @@ container, lets you handle this use case cleanly. **** You can read more about the motivation for Method Injection in -https://spring.io/blog/2004/08/06/method-injection/[this blog entry]. +{spring-site-blog}/2004/08/06/method-injection/[this blog entry]. **** @@ -120,8 +120,6 @@ dynamically generate a subclass that overrides the method. subclasses cannot be `final`, and the method to be overridden cannot be `final`, either. * Unit-testing a class that has an `abstract` method requires you to subclass the class yourself and to supply a stub implementation of the `abstract` method. -* Concrete methods are also necessary for component scanning, which requires concrete - classes to pick up. * A further key limitation is that lookup methods do not work with factory methods and in particular not with `@Bean` methods in configuration classes, since, in that case, the container is not in charge of creating the instance and therefore cannot create @@ -137,7 +135,7 @@ the reworked example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages",fold="none"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages",fold="none"] ---- package fiona.apple; @@ -160,7 +158,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages",fold="none"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages",fold="none"] ---- package fiona.apple @@ -201,7 +199,7 @@ the original class. Consider the following example: - + @@ -220,7 +218,7 @@ method through the `@Lookup` annotation, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public abstract class CommandManager { @@ -237,7 +235,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- abstract class CommandManager { @@ -260,7 +258,7 @@ declared return type of the lookup method: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public abstract class CommandManager { @@ -277,7 +275,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- abstract class CommandManager { @@ -293,11 +291,6 @@ Kotlin:: ---- ====== -Note that you should typically declare such annotated lookup methods with a concrete -stub implementation, in order for them to be compatible with Spring's component -scanning rules where abstract classes get ignored by default. This limitation does not -apply to explicitly registered or explicitly imported bean classes. - [TIP] ==== Another way of accessing differently scoped target beans is an `ObjectFactory`/ @@ -324,7 +317,7 @@ the following class, which has a method called `computeValue` that we want to ov ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyValueCalculator { @@ -338,7 +331,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyValueCalculator { @@ -358,7 +351,7 @@ interface provides the new method definition, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- /** * meant to be used to override the existing computeValue(String) @@ -377,7 +370,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- /** * meant to be used to override the existing computeValue(String) diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc index ae7874fa329f..90fd4106a1ad 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc @@ -51,7 +51,7 @@ XML configuration: The preceding XML is more succinct. However, typos are discovered at runtime rather than design time, unless you use an IDE (such as https://www.jetbrains.com/idea/[IntelliJ -IDEA] or the https://spring.io/tools[Spring Tools for Eclipse]) +IDEA] or the {spring-site-tools}[Spring Tools for Eclipse]) that supports automatic property completion when you create bean definitions. Such IDE assistance is highly recommended. @@ -102,8 +102,8 @@ following snippet: ---- - - + + ---- @@ -168,8 +168,8 @@ listings shows how to use the `parent` attribute: [source,xml,indent=0,subs="verbatim,quotes"] ---- - - + + @@ -354,7 +354,7 @@ The following Java class and bean definition show how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SomeClass { @@ -368,7 +368,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SomeClass { lateinit var accounts: Map @@ -418,14 +418,14 @@ The preceding example is equivalent to the following Java code: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- exampleBean.setEmail(""); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- exampleBean.email = "" ---- @@ -449,14 +449,14 @@ The preceding configuration is equivalent to the following Java code: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- exampleBean.setEmail(null); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- exampleBean.email = null ---- @@ -582,7 +582,7 @@ it needs to be declared in the XML file even though it is not defined in an XSD (it exists inside the Spring core). For the rare cases where the constructor argument names are not available (usually if -the bytecode was compiled without debugging information), you can use fallback to the +the bytecode was compiled without the `-parameters` flag), you can fall back to the argument indexes, as follows: [source,xml,indent=0,subs="verbatim,quotes"] diff --git a/framework-docs/modules/ROOT/pages/core/beans/environment.adoc b/framework-docs/modules/ROOT/pages/core/beans/environment.adoc index 571688d20c51..3b1c8898bedd 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/environment.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/environment.adoc @@ -1,7 +1,7 @@ [[beans-environment]] = Environment Abstraction -The {api-spring-framework}/core/env/Environment.html[`Environment`] interface +The {spring-framework-api}/core/env/Environment.html[`Environment`] interface is an abstraction integrated in the container that models two key aspects of the application environment: xref:core/beans/environment.adoc#beans-definition-profiles[profiles] and xref:core/beans/environment.adoc#beans-property-source-abstraction[properties]. @@ -43,7 +43,7 @@ Consider the first use case in a practical application that requires a ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean public DataSource dataSource() { @@ -57,7 +57,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean fun dataSource(): DataSource { @@ -79,7 +79,7 @@ now looks like the following listing: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean(destroyMethod = "") public DataSource dataSource() throws Exception { @@ -90,7 +90,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean(destroyMethod = "") fun dataSource(): DataSource { @@ -118,7 +118,7 @@ situation B. We start by updating our configuration to reflect this need. [[beans-definition-profiles-java]] === Using `@Profile` -The {api-spring-framework}/context/annotation/Profile.html[`@Profile`] +The {spring-framework-api}/context/annotation/Profile.html[`@Profile`] annotation lets you indicate that a component is eligible for registration when one or more specified profiles are active. Using our preceding example, we can rewrite the `dataSource` configuration as follows: @@ -128,7 +128,7 @@ can rewrite the `dataSource` configuration as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("development") @@ -147,7 +147,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("development") @@ -171,7 +171,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("production") @@ -188,7 +188,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("production") @@ -233,7 +233,7 @@ of creating a custom composed annotation. The following example defines a custom ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -244,7 +244,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @@ -272,7 +272,7 @@ the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -300,7 +300,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -454,7 +454,7 @@ it programmatically against the `Environment` API which is available through an ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setActiveProfiles("development"); @@ -464,7 +464,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = AnnotationConfigApplicationContext().apply { environment.setActiveProfiles("development") @@ -491,14 +491,14 @@ activates multiple profiles: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ctx.getEnvironment().setActiveProfiles("profile1", "profile2"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- ctx.getEnvironment().setActiveProfiles("profile1", "profile2") ---- @@ -516,14 +516,14 @@ as the following example shows: [[beans-definition-profiles-default]] === Default Profile -The default profile represents the profile that is enabled by default. Consider the -following example: +The default profile represents the profile that is enabled if no profile is active. Consider +the following example: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("default") @@ -541,7 +541,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("default") @@ -558,9 +558,9 @@ Kotlin:: ---- ====== -If no profile is active, the `dataSource` is created. You can see this -as a way to provide a default definition for one or more beans. If any -profile is enabled, the default profile does not apply. +If xref:#beans-definition-profiles-enable[no profile is active], the `dataSource` is +created. You can see this as a way to provide a default definition for one or more +beans. If any profile is enabled, the default profile does not apply. The name of the default profile is `default`. You can change the name of the default profile by using `setDefaultProfiles()` on the `Environment` or, @@ -578,7 +578,7 @@ hierarchy of property sources. Consider the following listing: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext ctx = new GenericApplicationContext(); Environment env = ctx.getEnvironment(); @@ -588,7 +588,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = GenericApplicationContext() val env = ctx.environment @@ -599,17 +599,17 @@ Kotlin:: In the preceding snippet, we see a high-level way of asking Spring whether the `my-property` property is defined for the current environment. To answer this question, the `Environment` object performs -a search over a set of {api-spring-framework}/core/env/PropertySource.html[`PropertySource`] +a search over a set of {spring-framework-api}/core/env/PropertySource.html[`PropertySource`] objects. A `PropertySource` is a simple abstraction over any source of key-value pairs, and -Spring's {api-spring-framework}/core/env/StandardEnvironment.html[`StandardEnvironment`] +Spring's {spring-framework-api}/core/env/StandardEnvironment.html[`StandardEnvironment`] is configured with two PropertySource objects -- one representing the set of JVM system properties (`System.getProperties()`) and one representing the set of system environment variables (`System.getenv()`). NOTE: These default property sources are present for `StandardEnvironment`, for use in standalone -applications. {api-spring-framework}/web/context/support/StandardServletEnvironment.html[`StandardServletEnvironment`] +applications. {spring-framework-api}/web/context/support/StandardServletEnvironment.html[`StandardServletEnvironment`] is populated with additional default property sources including servlet config, servlet -context parameters, and a {api-spring-framework}/jndi/JndiPropertySource.html[`JndiPropertySource`] +context parameters, and a {spring-framework-api}/jndi/JndiPropertySource.html[`JndiPropertySource`] if JNDI is available. Concretely, when you use the `StandardEnvironment`, the call to `env.containsProperty("my-property")` @@ -643,7 +643,7 @@ current `Environment`. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ConfigurableApplicationContext ctx = new GenericApplicationContext(); MutablePropertySources sources = ctx.getEnvironment().getPropertySources(); @@ -652,7 +652,7 @@ sources.addFirst(new MyPropertySource()); Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = GenericApplicationContext() val sources = ctx.environment.propertySources @@ -663,7 +663,7 @@ Kotlin:: In the preceding code, `MyPropertySource` has been added with highest precedence in the search. If it contains a `my-property` property, the property is detected and returned, in favor of any `my-property` property in any other `PropertySource`. The -{api-spring-framework}/core/env/MutablePropertySources.html[`MutablePropertySources`] +{spring-framework-api}/core/env/MutablePropertySources.html[`MutablePropertySources`] API exposes a number of methods that allow for precise manipulation of the set of property sources. @@ -672,7 +672,7 @@ property sources. [[beans-using-propertysource]] == Using `@PropertySource` -The {api-spring-framework}/context/annotation/PropertySource.html[`@PropertySource`] +The {spring-framework-api}/context/annotation/PropertySource.html[`@PropertySource`] annotation provides a convenient and declarative mechanism for adding a `PropertySource` to Spring's `Environment`. @@ -684,7 +684,7 @@ a call to `testBean.getName()` returns `myTestBean`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @PropertySource("classpath:/com/myco/app.properties") @@ -704,7 +704,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @PropertySource("classpath:/com/myco/app.properties") @@ -729,7 +729,7 @@ environment, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties") @@ -749,7 +749,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @PropertySource("classpath:/com/\${my.placeholder:default/path}/app.properties") 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 a82606dc0e9b..cf9e68e3a8eb 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc @@ -22,8 +22,8 @@ in which these `BeanPostProcessor` instances run by setting the `order` property You can set this property only if the `BeanPostProcessor` implements the `Ordered` interface. If you write your own `BeanPostProcessor`, you should consider implementing the `Ordered` interface, too. For further details, see the javadoc of the -{api-spring-framework}/beans/factory/config/BeanPostProcessor.html[`BeanPostProcessor`] -and {api-spring-framework}/core/Ordered.html[`Ordered`] interfaces. See also the note on +{spring-framework-api}/beans/factory/config/BeanPostProcessor.html[`BeanPostProcessor`] +and {spring-framework-api}/core/Ordered.html[`Ordered`] interfaces. See also the note on xref:core/beans/factory-extension.adoc#beans-factory-programmatically-registering-beanpostprocessors[programmatic registration of `BeanPostProcessor` instances]. [NOTE] @@ -123,7 +123,7 @@ The following listing shows the custom `BeanPostProcessor` implementation class ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package scripting; @@ -145,7 +145,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package scripting @@ -205,7 +205,7 @@ The following Java application runs the preceding code and configuration: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -224,7 +224,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -272,8 +272,8 @@ which these `BeanFactoryPostProcessor` instances run by setting the `order` prop However, you can only set this property if the `BeanFactoryPostProcessor` implements the `Ordered` interface. If you write your own `BeanFactoryPostProcessor`, you should consider implementing the `Ordered` interface, too. See the javadoc of the -{api-spring-framework}/beans/factory/config/BeanFactoryPostProcessor.html[`BeanFactoryPostProcessor`] -and {api-spring-framework}/core/Ordered.html[`Ordered`] interfaces for more details. +{spring-framework-api}/beans/factory/config/BeanFactoryPostProcessor.html[`BeanFactoryPostProcessor`] +and {spring-framework-api}/core/Ordered.html[`Ordered`] interfaces for more details. [NOTE] ==== @@ -439,7 +439,7 @@ dataSource.url=jdbc:mysql:mydb ---- This example file can be used with a container definition that contains a bean called -`dataSource` that has `driver` and `url` properties. +`dataSource` that has `driverClassName` and `url` properties. Compound property names are also supported, as long as every component of the path except the final property being overridden is already non-null (presumably initialized diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc index c23676f938e7..f6d310715a8d 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc @@ -72,7 +72,7 @@ no-argument signature. With Java configuration, you can use the `initMethod` att ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ExampleBean { @@ -84,7 +84,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleBean { @@ -107,7 +107,7 @@ The preceding example has almost exactly the same effect as the following exampl ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class AnotherExampleBean implements InitializingBean { @@ -120,7 +120,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class AnotherExampleBean : InitializingBean { @@ -144,7 +144,7 @@ based on the given configuration but no further activity with external bean acce Otherwise there is a risk for an initialization deadlock. For a scenario where expensive post-initialization activity is to be triggered, -e.g. asynchronous database preparation steps, your bean should either implement +for example, asynchronous database preparation steps, your bean should either implement `SmartInitializingSingleton.afterSingletonsInstantiated()` or rely on the context refresh event: implementing `ApplicationListener` or declaring its annotation equivalent `@EventListener(ContextRefreshedEvent.class)`. @@ -187,7 +187,7 @@ xref:core/beans/java/bean-annotation.adoc#beans-java-lifecycle-callbacks[Receivi ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ExampleBean { @@ -199,7 +199,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleBean { @@ -221,7 +221,7 @@ The preceding definition has almost exactly the same effect as the following def ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class AnotherExampleBean implements DisposableBean { @@ -234,7 +234,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class AnotherExampleBean : DisposableBean { @@ -295,7 +295,7 @@ following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class DefaultBlogService implements BlogService { @@ -316,7 +316,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class DefaultBlogService : BlogService { @@ -551,7 +551,7 @@ declared on the `ConfigurableApplicationContext` interface, as the following exa ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -573,7 +573,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.context.support.ClassPathXmlApplicationContext @@ -592,6 +592,40 @@ Kotlin:: +[[beans-factory-thread-safety]] +=== Thread Safety and Visibility + +The Spring core container publishes created singleton instances in a thread-safe manner, +guarding access through a singleton lock and guaranteeing visibility in other threads. + +As a consequence, application-provided bean classes do not have to be concerned about the +visibility of their initialization state. Regular configuration fields do not have to be +marked as `volatile` as long as they are only mutated during the initialization phase, +providing visibility guarantees similar to `final` even for setter-based configuration +state that is mutable during that initial phase. If such fields get changed after the +bean creation phase and its subsequent initial publication, they need to be declared as +`volatile` or guarded by a common lock whenever accessed. + +Note that concurrent access to such configuration state in singleton bean instances, +for example, for controller instances or repository instances, is perfectly thread-safe after +such safe initial publication from the container side. This includes common singleton +`FactoryBean` instances which are processed within the general singleton lock as well. + +For destruction callbacks, the configuration state remains thread-safe but any runtime +state accumulated between initialization and destruction should be kept in thread-safe +structures (or in `volatile` fields for simple cases) as per common Java guidelines. + +Deeper `Lifecycle` integration as shown above involves runtime-mutable state such as +a `runnable` field which will have to be declared as `volatile`. While the common +lifecycle callbacks follow a certain order, for example, a start callback is guaranteed to +only happen after full initialization and a stop callback only after an initial start, +there is a special case with the common stop before destroy arrangement: It is strongly +recommended that the internal state in any such bean also allows for an immediate +destroy callback without a preceding stop since this may happen during an extraordinary +shutdown after a cancelled bootstrap or in case of a stop timeout caused by another bean. + + + [[beans-factory-aware]] == `ApplicationContextAware` and `BeanNameAware` diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc index 8243d755d1e1..df5b753514a8 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc @@ -51,7 +51,7 @@ The following table describes the supported scopes: NOTE: A thread scope is available but is not registered by default. For more information, see the documentation for -{api-spring-framework}/context/support/SimpleThreadScope.html[`SimpleThreadScope`]. +{spring-framework-api}/context/support/SimpleThreadScope.html[`SimpleThreadScope`]. For instructions on how to register this or any other custom scope, see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-custom-using[Using a Custom Scope]. @@ -251,7 +251,7 @@ to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RequestScope @Component @@ -262,7 +262,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RequestScope @Component @@ -301,7 +301,7 @@ When using annotation-driven components or Java configuration, you can use the ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SessionScope @Component @@ -312,7 +312,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SessionScope @Component @@ -324,7 +324,6 @@ Kotlin:: - [[beans-factory-scopes-application]] === Application Scope @@ -351,7 +350,7 @@ following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ApplicationScope @Component @@ -362,7 +361,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ApplicationScope @Component @@ -374,7 +373,6 @@ Kotlin:: - [[beans-factory-scopes-websocket]] === WebSocket Scope @@ -384,7 +382,6 @@ xref:web/websocket/stomp/scope.adoc[WebSocket scope] for more details. - [[beans-factory-scopes-other-injection]] === Scoped Beans as Dependencies @@ -544,6 +541,19 @@ see xref:core/aop/proxying.adoc[Proxying Mechanisms]. +[[beans-factory-scopes-injection]] +=== Injecting Request/Session References Directly + +As an alternative to factory scopes, a Spring `WebApplicationContext` also supports +the injection of `HttpServletRequest`, `HttpServletResponse`, `HttpSession`, +`WebRequest` and (if JSF is present) `FacesContext` and `ExternalContext` into +Spring-managed beans, simply through type-based autowiring next to regular injection +points for other beans. Spring generally injects proxies for such request and session +objects which has the advantage of working in singleton beans and serializable beans +as well, similar to scoped proxies for factory-scoped beans. + + + [[beans-factory-scopes-custom]] == Custom Scopes @@ -559,7 +569,7 @@ To integrate your custom scopes into the Spring container, you need to implement `org.springframework.beans.factory.config.Scope` interface, which is described in this section. For an idea of how to implement your own scopes, see the `Scope` implementations that are supplied with the Spring Framework itself and the -{api-spring-framework}/beans/factory/config/Scope.html[`Scope`] javadoc, +{spring-framework-api}/beans/factory/config/Scope.html[`Scope`] javadoc, which explains the methods you need to implement in more detail. The `Scope` interface has four methods to get objects from the scope, remove them from @@ -574,14 +584,14 @@ underlying scope: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Object get(String name, ObjectFactory objectFactory) ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun get(name: String, objectFactory: ObjectFactory<*>): Any ---- @@ -596,14 +606,14 @@ the underlying scope: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Object remove(String name) ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun remove(name: String): Any ---- @@ -616,20 +626,20 @@ destroyed or when the specified object in the scope is destroyed: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- void registerDestructionCallback(String name, Runnable destructionCallback) ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun registerDestructionCallback(name: String, destructionCallback: Runnable) ---- ====== -See the {api-spring-framework}/beans/factory/config/Scope.html#registerDestructionCallback[javadoc] +See the {spring-framework-api}/beans/factory/config/Scope.html#registerDestructionCallback[javadoc] or a Spring scope implementation for more information on destruction callbacks. The following method obtains the conversation identifier for the underlying scope: @@ -638,14 +648,14 @@ The following method obtains the conversation identifier for the underlying scop ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String getConversationId() ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun getConversationId(): String ---- @@ -667,14 +677,14 @@ method to register a new `Scope` with the Spring container: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- void registerScope(String scopeName, Scope scope); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun registerScope(scopeName: String, scope: Scope) ---- @@ -700,7 +710,7 @@ implementations. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Scope threadScope = new SimpleThreadScope(); beanFactory.registerScope("thread", threadScope); @@ -708,7 +718,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val threadScope = SimpleThreadScope() beanFactory.registerScope("thread", threadScope) diff --git a/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc b/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc index 84fce22f428d..969cbed145f6 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc @@ -1,22 +1,22 @@ [[beans-introduction]] = Introduction to the Spring IoC Container and Beans -This chapter covers the Spring Framework implementation of the Inversion of Control -(IoC) principle. IoC is also known as dependency injection (DI). It is a process whereby -objects define their dependencies (that is, the other objects they work with) only through -constructor arguments, arguments to a factory method, or properties that are set on the -object instance after it is constructed or returned from a factory method. The container +This chapter covers the Spring Framework implementation of the Inversion of Control (IoC) +principle. Dependency injection (DI) is a specialized form of IoC, whereby objects define +their dependencies (that is, the other objects they work with) only through constructor +arguments, arguments to a factory method, or properties that are set on the object +instance after it is constructed or returned from a factory method. The IoC container then injects those dependencies when it creates the bean. This process is fundamentally -the inverse (hence the name, Inversion of Control) of the bean itself -controlling the instantiation or location of its dependencies by using direct -construction of classes or a mechanism such as the Service Locator pattern. +the inverse (hence the name, Inversion of Control) of the bean itself controlling the +instantiation or location of its dependencies by using direct construction of classes or +a mechanism such as the Service Locator pattern. The `org.springframework.beans` and `org.springframework.context` packages are the basis for Spring Framework's IoC container. The -{api-spring-framework}/beans/factory/BeanFactory.html[`BeanFactory`] +{spring-framework-api}/beans/factory/BeanFactory.html[`BeanFactory`] interface provides an advanced configuration mechanism capable of managing any type of object. -{api-spring-framework}/context/ApplicationContext.html[`ApplicationContext`] +{spring-framework-api}/context/ApplicationContext.html[`ApplicationContext`] is a sub-interface of `BeanFactory`. It adds: * Easier integration with Spring's AOP features diff --git a/framework-docs/modules/ROOT/pages/core/beans/java.adoc b/framework-docs/modules/ROOT/pages/core/beans/java.adoc index 9cb9f492b6e7..8f3f9f7aac0b 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java.adoc @@ -3,17 +3,5 @@ :page-section-summary-toc: 1 This section covers how to use annotations in your Java code to configure the Spring -container. It includes the following topics: - -* xref:core/beans/java/basic-concepts.adoc[Basic Concepts: `@Bean` and `@Configuration`] -* xref:core/beans/java/instantiating-container.adoc[Instantiating the Spring Container by Using `AnnotationConfigApplicationContext`] -* xref:core/beans/java/bean-annotation.adoc[Using the `@Bean` Annotation] -* xref:core/beans/java/configuration-annotation.adoc[Using the `@Configuration` annotation] -* xref:core/beans/java/composing-configuration-classes.adoc[Composing Java-based Configurations] -* xref:core/beans/environment.adoc#beans-definition-profiles[Bean Definition Profiles] -* xref:core/beans/environment.adoc#beans-property-source-abstraction[`PropertySource` Abstraction] -* xref:core/beans/environment.adoc#beans-using-propertysource[Using `@PropertySource`] -* xref:core/beans/environment.adoc#beans-placeholder-resolution-in-statements[Placeholder Resolution in Statements] - - +container. diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/basic-concepts.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/basic-concepts.adoc index 80794f693e94..80e28de0a8f5 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java/basic-concepts.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java/basic-concepts.adoc @@ -19,7 +19,7 @@ The simplest possible `@Configuration` class reads as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -33,7 +33,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -55,33 +55,34 @@ The preceding `AppConfig` class is equivalent to the following Spring `` ---- -.Full @Configuration vs "`lite`" @Bean mode? +.@Configuration classes with or without local calls between @Bean methods? **** -When `@Bean` methods are declared within classes that are not annotated with -`@Configuration`, they are referred to as being processed in a "`lite`" mode. Bean methods -declared on a bean that is not annotated with `@Configuration` are considered to be "`lite`", -with a different primary purpose of the containing class and a `@Bean` method -being a sort of bonus there. For example, service components may expose management views -to the container through an additional `@Bean` method on each applicable component class. -In such scenarios, `@Bean` methods are a general-purpose factory method mechanism. - -Unlike full `@Configuration`, lite `@Bean` methods cannot declare inter-bean dependencies. -Instead, they operate on their containing component's internal state and, optionally, on -arguments that they may declare. Such a `@Bean` method should therefore not invoke other -`@Bean` methods. Each such method is literally only a factory method for a particular -bean reference, without any special runtime semantics. The positive side-effect here is -that no CGLIB subclassing has to be applied at runtime, so there are no limitations in -terms of class design (that is, the containing class may be `final` and so forth). - In common scenarios, `@Bean` methods are to be declared within `@Configuration` classes, -ensuring that "`full`" mode is always used and that cross-method references therefore -get redirected to the container's lifecycle management. This prevents the same -`@Bean` method from accidentally being invoked through a regular Java call, which helps -to reduce subtle bugs that can be hard to track down when operating in "`lite`" mode. +ensuring that full configuration class processing applies and that cross-method +references therefore get redirected to the container's lifecycle management. +This prevents the same `@Bean` method from accidentally being invoked through a regular +Java method call, which helps to reduce subtle bugs that can be hard to track down. + +When `@Bean` methods are declared within classes that are not annotated with +`@Configuration`, or when `@Configuration(proxyBeanMethods=false)` is declared, +they are referred to as being processed in a "lite" mode. In such scenarios, +`@Bean` methods are effectively a general-purpose factory method mechanism without +special runtime processing (that is, without generating a CGLIB subclass for it). +A custom Java call to such a method will not get intercepted by the container and +therefore behaves just like a regular method call, creating a new instance every time +rather than reusing an existing singleton (or scoped) instance for the given bean. + +As a consequence, `@Bean` methods on classes without runtime proxying are not meant to +declare inter-bean dependencies at all. Instead, they are expected to operate on their +containing component's fields and, optionally, on arguments that a factory method may +declare in order to receive autowired collaborators. Such a `@Bean` method therefore +never needs to invoke other `@Bean` methods; every such call can be expressed through +a factory method argument instead. The positive side-effect here is that no CGLIB +subclassing has to be applied at runtime, reducing the overhead and the footprint. **** The `@Bean` and `@Configuration` annotations are discussed in depth in the following sections. -First, however, we cover the various ways of creating a spring container by using +First, however, we cover the various ways of creating a Spring container by using Java-based configuration. diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/bean-annotation.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/bean-annotation.adoc index dd35cefa9d33..dcc181d51586 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java/bean-annotation.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java/bean-annotation.adoc @@ -25,7 +25,7 @@ the method name. The following example shows a `@Bean` method declaration: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -39,7 +39,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -75,7 +75,7 @@ configurations by implementing interfaces with bean definitions on default metho ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface BaseConfig { @@ -99,7 +99,7 @@ return type, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -113,7 +113,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -153,7 +153,7 @@ parameter, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -167,7 +167,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -211,7 +211,7 @@ on the `bean` element, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class BeanOne { @@ -244,7 +244,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class BeanOne { @@ -291,7 +291,7 @@ The following example shows how to prevent an automatic destruction callback for ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean(destroyMethod = "") public DataSource dataSource() throws NamingException { @@ -301,7 +301,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean(destroyMethod = "") fun dataSource(): DataSource { @@ -326,7 +326,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -344,7 +344,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -382,7 +382,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class MyConfiguration { @@ -397,7 +397,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class MyConfiguration { @@ -431,7 +431,7 @@ it resembles the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // an HTTP Session-scoped bean exposed as a proxy @Bean @@ -451,7 +451,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // an HTTP Session-scoped bean exposed as a proxy @Bean @@ -479,7 +479,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -493,7 +493,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -517,7 +517,7 @@ The following example shows how to set a number of aliases for a bean: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -531,7 +531,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -552,14 +552,14 @@ Sometimes, it is helpful to provide a more detailed textual description of a bea be particularly useful when beans are exposed (perhaps through JMX) for monitoring purposes. To add a description to a `@Bean`, you can use the -{api-spring-framework}/context/annotation/Description.html[`@Description`] +{spring-framework-api}/context/annotation/Description.html[`@Description`] annotation, as the following example shows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -574,7 +574,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc index 638c8736b34f..0ef19ea633c3 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc @@ -16,7 +16,7 @@ another configuration class, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class ConfigA { @@ -40,7 +40,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class ConfigA { @@ -67,7 +67,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class); @@ -80,7 +80,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -116,14 +116,14 @@ the configuration model, in that references to other beans must be valid Java sy Fortunately, solving this problem is simple. As xref:core/beans/java/bean-annotation.adoc#beans-java-dependencies[we already discussed], a `@Bean` method can have an arbitrary number of parameters that describe the bean -dependencies. Consider the following more real-world scenario with several `@Configuration` +dependencies. Consider the following more realistic scenario with several `@Configuration` classes, each depending on beans declared in the others: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class ServiceConfig { @@ -163,7 +163,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -219,13 +219,13 @@ parameter-based injection, as in the preceding example. Avoid access to locally defined beans within a `@PostConstruct` method on the same configuration class. This effectively leads to a circular reference since non-static `@Bean` methods semantically require a fully initialized configuration class instance to be called on. With circular references -disallowed (e.g. in Spring Boot 2.6+), this may trigger a `BeanCurrentlyInCreationException`. +disallowed (for example, in Spring Boot 2.6+), this may trigger a `BeanCurrentlyInCreationException`. Also, be particularly careful with `BeanPostProcessor` and `BeanFactoryPostProcessor` definitions through `@Bean`. Those should usually be declared as `static @Bean` methods, not triggering the instantiation of their containing configuration class. Otherwise, `@Autowired` and `@Value` may not work on the configuration class itself, since it is possible to create it as a bean instance earlier than -{api-spring-framework}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`]. +{spring-framework-api}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`]. ==== The following example shows how one bean can be autowired to another bean: @@ -234,7 +234,7 @@ The following example shows how one bean can be autowired to another bean: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class ServiceConfig { @@ -283,7 +283,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -331,14 +331,16 @@ TIP: Constructor injection in `@Configuration` classes is only supported as of S Framework 4.3. Note also that there is no need to specify `@Autowired` if the target bean defines only one constructor. -.[[beans-java-injecting-imported-beans-fq]]Fully-qualifying imported beans for ease of navigation --- +[discrete] +[[beans-java-injecting-imported-beans-fq]] +==== Fully-qualifying imported beans for ease of navigation + In the preceding scenario, using `@Autowired` works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at `ServiceConfig`, how do you know exactly where the `@Autowired AccountRepository` bean is declared? It is not explicit in the code, and this may be just fine. Remember that the -https://spring.io/tools[Spring Tools for Eclipse] provides tooling that +{spring-site-tools}[Spring Tools for Eclipse] provides tooling that can render graphs showing how everything is wired, which may be all you need. Also, your Java IDE can easily find all declarations and uses of the `AccountRepository` type and quickly show you the location of `@Bean` methods that return that type. @@ -351,7 +353,7 @@ configuration classes themselves. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class ServiceConfig { @@ -369,7 +371,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class ServiceConfig { @@ -395,7 +397,7 @@ abstract class-based `@Configuration` classes. Consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class ServiceConfig { @@ -445,7 +447,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -501,12 +503,48 @@ Now `ServiceConfig` is loosely coupled with respect to the concrete get a type hierarchy of `RepositoryConfig` implementations. In this way, navigating `@Configuration` classes and their dependencies becomes no different than the usual process of navigating interface-based code. --- -TIP: If you want to influence the startup creation order of certain beans, consider -declaring some of them as `@Lazy` (for creation on first access instead of on startup) -or as `@DependsOn` certain other beans (making sure that specific other beans are -created before the current bean, beyond what the latter's direct dependencies imply). + +[[beans-java-startup]] +== Influencing the Startup of `@Bean`-defined Singletons + +If you want to influence the startup creation order of certain singleton beans, consider +declaring some of them as `@Lazy` for creation on first access instead of on startup. + +`@DependsOn` forces certain other beans to be initialized first, making sure that +the specified beans are created before the current bean, beyond what the latter's +direct dependencies imply. + +[[beans-java-startup-background]] +=== Background Initialization + +As of 6.2, there is a background initialization option: `@Bean(bootstrap=BACKGROUND)` +allows for singling out specific beans for background initialization, covering the +entire bean creation step for each such bean on context startup. + +Dependent beans with non-lazy injection points automatically wait for the bean instance +to be completed. All regular background initializations are forced to complete at the end +of context startup. Only beans additionally marked as `@Lazy` are allowed to be completed +later (up until the first actual access). + +Background initialization typically goes together with `@Lazy` (or `ObjectProvider`) +injection points in dependent beans. Otherwise, the main bootstrap thread is going to +block when an actual background-initialized bean instance needs to be injected early. + +This form of concurrent startup applies to individual beans: if such a bean depends on +other beans, they need to have been initialized already, either simply through being +declared earlier or through `@DependsOn` which enforces initialization in the main +bootstrap thread before background initialization for the affected bean is triggered. + +[NOTE] +==== +A `bootstrapExecutor` bean of type `Executor` must be declared for background +bootstrapping to be actually active. Otherwise, the background markers will be ignored at +runtime. + +The bootstrap executor may be a bounded executor just for startup purposes or a shared +thread pool which serves for other purposes as well. +==== [[beans-java-conditional]] @@ -519,7 +557,7 @@ profile has been enabled in the Spring `Environment` (see xref:core/beans/enviro for details). The `@Profile` annotation is actually implemented by using a much more flexible annotation -called {api-spring-framework}/context/annotation/Conditional.html[`@Conditional`]. +called {spring-framework-api}/context/annotation/Conditional.html[`@Conditional`]. The `@Conditional` annotation indicates specific `org.springframework.context.annotation.Condition` implementations that should be consulted before a `@Bean` is registered. @@ -532,7 +570,7 @@ method that returns `true` or `false`. For example, the following listing shows ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { @@ -540,7 +578,7 @@ Java:: MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { - if (context.getEnvironment().acceptsProfiles(((String[]) value))) { + if (context.getEnvironment().matchesProfiles((String[]) value)) { return true; } } @@ -552,14 +590,14 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean { // Read the @Profile annotation attributes val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name) if (attrs != null) { for (value in attrs["value"]!!) { - if (context.environment.acceptsProfiles(Profiles.of(*value as Array))) { + if (context.environment.matchesProfiles(*value as Array)) { return true } } @@ -570,7 +608,7 @@ Kotlin:: ---- ====== -See the {api-spring-framework}/context/annotation/Conditional.html[`@Conditional`] +See the {spring-framework-api}/context/annotation/Conditional.html[`@Conditional`] javadoc for more detail. @@ -594,22 +632,24 @@ that uses Spring XML, it is easier to create `@Configuration` classes on an as-needed basis and include them from the existing XML files. Later in this section, we cover the options for using `@Configuration` classes in this kind of "`XML-centric`" situation. -.[[beans-java-combining-xml-centric-declare-as-bean]]Declaring `@Configuration` classes as plain Spring `` elements --- -Remember that `@Configuration` classes are ultimately bean definitions in the -container. In this series examples, we create a `@Configuration` class named `AppConfig` and +[discrete] +[[beans-java-combining-xml-centric-declare-as-bean]] +==== Declaring `@Configuration` classes as plain Spring `` elements + +Remember that `@Configuration` classes are ultimately bean definitions in the container. +In this series of examples, we create a `@Configuration` class named `AppConfig` and include it within `system-test-config.xml` as a `` definition. Because `` is switched on, the container recognizes the `@Configuration` annotation and processes the `@Bean` methods declared in `AppConfig` properly. -The following example shows an ordinary configuration class in Java: +The following example shows the `AppConfig` configuration class in Java and Kotlin: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -624,14 +664,14 @@ Java:: @Bean public TransferService transferService() { - return new TransferService(accountRepository()); + return new TransferServiceImpl(accountRepository()); } } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -657,6 +697,7 @@ The following example shows part of a sample `system-test-config.xml` file: + @@ -682,7 +723,7 @@ jdbc.password= ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml"); @@ -693,7 +734,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun main() { val ctx = ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml") @@ -703,20 +744,20 @@ Kotlin:: ---- ====== - -NOTE: In `system-test-config.xml` file, the `AppConfig` `` does not declare an `id` -element. While it would be acceptable to do so, it is unnecessary, given that no other bean +NOTE: In the `system-test-config.xml` file, the `AppConfig` `` does not declare an `id` +attribute. While it would be acceptable to do so, it is unnecessary, given that no other bean ever refers to it, and it is unlikely to be explicitly fetched from the container by name. Similarly, the `DataSource` bean is only ever autowired by type, so an explicit bean `id` is not strictly required. --- -.[[beans-java-combining-xml-centric-component-scan]] Using to pick up `@Configuration` classes --- +[discrete] +[[beans-java-combining-xml-centric-component-scan]] +==== Using to pick up `@Configuration` classes + Because `@Configuration` is meta-annotated with `@Component`, `@Configuration`-annotated classes are automatically candidates for component scanning. Using the same scenario as -described in the previous example, we can redefine `system-test-config.xml` to take advantage of component-scanning. -Note that, in this case, we need not explicitly declare +described in the previous example, we can redefine `system-test-config.xml` to take +advantage of component-scanning. Note that, in this case, we need not explicitly declare ``, because `` enables the same functionality. @@ -727,6 +768,7 @@ The following example shows the modified `system-test-config.xml` file: + @@ -736,25 +778,23 @@ The following example shows the modified `system-test-config.xml` file: ---- --- [[beans-java-combining-java-centric]] === `@Configuration` Class-centric Use of XML with `@ImportResource` In applications where `@Configuration` classes are the primary mechanism for configuring -the container, it is still likely necessary to use at least some XML. In these -scenarios, you can use `@ImportResource` and define only as much XML as you need. Doing -so achieves a "`Java-centric`" approach to configuring the container and keeps XML to a -bare minimum. The following example (which includes a configuration class, an XML file -that defines a bean, a properties file, and the `main` class) shows how to use -the `@ImportResource` annotation to achieve "`Java-centric`" configuration that uses XML -as needed: +the container, it may still be necessary to use at least some XML. In such scenarios, you +can use `@ImportResource` and define only as much XML as you need. Doing so achieves a +"`Java-centric`" approach to configuring the container and keeps XML to a bare minimum. +The following example (which includes a configuration class, an XML file that defines a +bean, a properties file, and the `main()` method) shows how to use the `@ImportResource` +annotation to achieve "`Java-centric`" configuration that uses XML as needed: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ImportResource("classpath:/com/acme/properties-config.xml") @@ -773,12 +813,23 @@ Java:: public DataSource dataSource() { return new DriverManagerDataSource(url, username, password); } + + @Bean + public AccountRepository accountRepository(DataSource dataSource) { + return new JdbcAccountRepository(dataSource); + } + + @Bean + public TransferService transferService(AccountRepository accountRepository) { + return new TransferServiceImpl(accountRepository); + } + } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ImportResource("classpath:/com/acme/properties-config.xml") @@ -797,21 +848,32 @@ Kotlin:: fun dataSource(): DataSource { return DriverManagerDataSource(url, username, password) } + + @Bean + fun accountRepository(dataSource: DataSource): AccountRepository { + return JdbcAccountRepository(dataSource) + } + + @Bean + fun transferService(accountRepository: AccountRepository): TransferService { + return TransferServiceImpl(accountRepository) + } + } ---- ====== +.properties-config.xml [source,xml,indent=0,subs="verbatim,quotes"] ---- - properties-config.xml ---- +.jdbc.properties [literal,subs="verbatim,quotes"] ---- -jdbc.properties jdbc.url=jdbc:hsqldb:hsql://localhost/xdb jdbc.username=sa jdbc.password= @@ -821,7 +883,7 @@ jdbc.password= ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); @@ -832,7 +894,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -844,5 +906,3 @@ Kotlin:: ---- ====== - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/configuration-annotation.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/configuration-annotation.adoc index d265db7e7585..0ed5254a1a82 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java/configuration-annotation.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java/configuration-annotation.adoc @@ -17,7 +17,7 @@ as having one bean method call another, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -36,7 +36,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -72,7 +72,7 @@ following example shows how to use lookup method injection: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public abstract class CommandManager { public Object process(Object commandState) { @@ -90,7 +90,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- abstract class CommandManager { fun process(commandState: Any): Any { @@ -115,7 +115,7 @@ the abstract `createCommand()` method is overridden in such a way that it looks ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean @Scope("prototype") @@ -139,7 +139,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean @Scope("prototype") @@ -172,7 +172,7 @@ Consider the following example, which shows a `@Bean` annotated method being cal ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -200,7 +200,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/instantiating-container.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/instantiating-container.adoc index 3a1d9b9176cc..3618e839043f 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java/instantiating-container.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java/instantiating-container.adoc @@ -27,7 +27,7 @@ XML-free usage of the Spring container, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); @@ -38,7 +38,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -58,7 +58,7 @@ as input to the constructor, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class); @@ -69,7 +69,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -97,7 +97,7 @@ example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); @@ -111,7 +111,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -136,7 +136,7 @@ To enable component scanning, you can annotate your `@Configuration` class as fo ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = "com.acme") // <1> @@ -148,7 +148,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = ["com.acme"]) // <1> @@ -183,7 +183,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); @@ -195,7 +195,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun main() { val ctx = AnnotationConfigApplicationContext() @@ -278,7 +278,7 @@ init-param): NOTE: For programmatic use cases, a `GenericWebApplicationContext` can be used as an alternative to `AnnotationConfigWebApplicationContext`. See the -{api-spring-framework}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] +{spring-framework-api}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] javadoc for details. 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/beans/standard-annotations.adoc b/framework-docs/modules/ROOT/pages/core/beans/standard-annotations.adoc index d9929bea3d03..666edbeb6142 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/standard-annotations.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/standard-annotations.adoc @@ -33,7 +33,7 @@ Instead of `@Autowired`, you can use `@jakarta.inject.Inject` as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject; @@ -55,7 +55,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject @@ -83,7 +83,7 @@ preceding example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject; import jakarta.inject.Provider; @@ -106,7 +106,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject @@ -131,7 +131,7 @@ you should use the `@Named` annotation, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject; import jakarta.inject.Named; @@ -151,7 +151,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject import jakarta.inject.Named @@ -190,7 +190,7 @@ a `required` attribute. The following pair of examples show how to use `@Inject` ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleMovieLister { @@ -203,7 +203,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -225,7 +225,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject; import jakarta.inject.Named; @@ -246,7 +246,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject import jakarta.inject.Named @@ -269,7 +269,7 @@ It is very common to use `@Component` without specifying a name for the componen ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject; import jakarta.inject.Named; @@ -290,7 +290,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import jakarta.inject.Inject import jakarta.inject.Named @@ -313,7 +313,7 @@ exact same way as when you use Spring annotations, as the following example show ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = "org.example") @@ -324,7 +324,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = ["org.example"]) diff --git a/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc b/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc index a66ade956c27..d25bd9769899 100644 --- a/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc +++ b/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc @@ -29,7 +29,7 @@ a `DataBuffer` implementation and that does not involve allocation. Note that WebFlux applications do not create a `DataBufferFactory` directly but instead access it through the `ServerHttpResponse` or the `ClientHttpRequest` on the client side. -The type of factory depends on the underlying client or server, e.g. +The type of factory depends on the underlying client or server, for example, `NettyDataBufferFactory` for Reactor Netty, `DefaultDataBufferFactory` for others. @@ -56,7 +56,7 @@ alternate between read and write. == `PooledDataBuffer` As explained in the Javadoc for -https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html[ByteBuffer], +{java-api}/java.base/java/nio/ByteBuffer.html[ByteBuffer], byte buffers can be direct or non-direct. Direct buffers may reside outside the Java heap which eliminates the need for copying for native I/O operations. That makes direct buffers particularly useful for receiving and sending data over a socket, but they're also more @@ -82,7 +82,7 @@ to use the convenience methods in `DataBufferUtils` that apply release or retain `DataBufferUtils` offers a number of utility methods to operate on data buffers: -* Join a stream of data buffers into a single buffer possibly with zero copy, e.g. via +* Join a stream of data buffers into a single buffer possibly with zero copy, for example, via composite buffers, if that's supported by the underlying byte buffer API. * Turn `InputStream` or NIO `Channel` into `Flux`, and vice versa a `Publisher` into `OutputStream` or NIO `Channel`. @@ -144,7 +144,7 @@ a serialization error occurs while populating the buffer with data. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- DataBuffer buffer = factory.allocateBuffer(); boolean release = true; @@ -162,7 +162,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val buffer = factory.allocateBuffer() var release = true diff --git a/framework-docs/modules/ROOT/pages/core/expressions.adoc b/framework-docs/modules/ROOT/pages/core/expressions.adoc index 9b02cc49a82b..7700929f7b60 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions.adoc @@ -1,17 +1,18 @@ [[expressions]] = Spring Expression Language (SpEL) -The Spring Expression Language ("`SpEL`" for short) is a powerful expression language that +The Spring Expression Language ("SpEL" for short) is a powerful expression language that supports querying and manipulating an object graph at runtime. The language syntax is -similar to Unified EL but offers additional features, most notably method invocation and -basic string templating functionality. +similar to the https://jakarta.ee/specifications/expression-language/[Jakarta Expression +Language] but offers additional features, most notably method invocation and basic string +templating functionality. While there are several other Java expression languages available -- OGNL, MVEL, and JBoss EL, to name a few -- the Spring Expression Language was created to provide the Spring community with a single well supported expression language that can be used across all the products in the Spring portfolio. Its language features are driven by the requirements of the projects in the Spring portfolio, including tooling requirements -for code completion support within the https://spring.io/tools[Spring Tools for Eclipse]. +for code completion support within the {spring-site-tools}[Spring Tools for Eclipse]. That said, SpEL is based on a technology-agnostic API that lets other expression language implementations be integrated, should the need arise. @@ -33,26 +34,24 @@ populate them are listed at the end of the chapter. The expression language supports the following functionality: * Literal expressions -* Boolean and relational operators -* Regular expressions -* Class expressions * Accessing properties, arrays, lists, and maps -* Method invocation -* Assignment -* Calling constructors -* Bean references -* Array construction * Inline lists * Inline maps -* Ternary operator +* Array construction +* Relational operators +* Regular expressions +* Logical operators +* String operators +* Mathematical operators +* Assignment +* Type expressions +* Method invocation +* Constructor invocation * Variables -* User-defined functions added to the context - * reflective invocation of `Method` - * various cases of `MethodHandle` +* User-defined functions +* Bean references +* Ternary, Elvis, and safe-navigation operators * Collection projection * Collection selection * Templated expressions - - - diff --git a/framework-docs/modules/ROOT/pages/core/expressions/beandef.adoc b/framework-docs/modules/ROOT/pages/core/expressions/beandef.adoc index 7617ab15ef70..a246534089cd 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/beandef.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/beandef.adoc @@ -1,219 +1,34 @@ [[expressions-beandef]] = Expressions in Bean Definitions -You can use SpEL expressions with XML-based or annotation-based configuration metadata for -defining `BeanDefinition` instances. In both cases, the syntax to define the expression is of the -form `#{ }`. - - - -[[expressions-beandef-xml-based]] -== XML Configuration - -A property or constructor argument value can be set by using expressions, as the following -example shows: - -[source,xml,indent=0,subs="verbatim"] ----- - - - - - ----- +You can use SpEL expressions with configuration metadata for defining bean instances. In both +cases, the syntax to define the expression is of the form `#{ }`. All beans in the application context are available as predefined variables with their common bean name. This includes standard context beans such as `environment` (of type `org.springframework.core.env.Environment`) as well as `systemProperties` and `systemEnvironment` (of type `Map`) for access to the runtime environment. -The following example shows access to the `systemProperties` bean as a SpEL variable: - -[source,xml,indent=0,subs="verbatim"] ----- - - - - - ----- - -Note that you do not have to prefix the predefined variable with the `#` symbol here. - -You can also refer to other bean properties by name, as the following example shows: - -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - - - - - - ----- - - - -[[expressions-beandef-annotation-based]] -== Annotation Configuration - To specify a default value, you can place the `@Value` annotation on fields, methods, -and method or constructor parameters. +and method or constructor parameters (or XML equivalent). The following example sets the default value of a field: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class FieldValueTestBean { - - @Value("#{ systemProperties['user.region'] }") - private String defaultLocale; - - public void setDefaultLocale(String defaultLocale) { - this.defaultLocale = defaultLocale; - } - - public String getDefaultLocale() { - return this.defaultLocale; - } - } ----- +include-code::./FieldValueTestBean[tag=snippet,indent=0] -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class FieldValueTestBean { - - @Value("#{ systemProperties['user.region'] }") - var defaultLocale: String? = null - } ----- -====== +Note that you do not have to prefix the predefined variable with the `#` symbol here. The following example shows the equivalent but on a property setter method: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class PropertyValueTestBean { - - private String defaultLocale; - - @Value("#{ systemProperties['user.region'] }") - public void setDefaultLocale(String defaultLocale) { - this.defaultLocale = defaultLocale; - } - - public String getDefaultLocale() { - return this.defaultLocale; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class PropertyValueTestBean { - - @Value("#{ systemProperties['user.region'] }") - var defaultLocale: String? = null - } ----- -====== +include-code::./PropertyValueTestBean[tag=snippet,indent=0] Autowired methods and constructors can also use the `@Value` annotation, as the following examples show: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class SimpleMovieLister { - - private MovieFinder movieFinder; - private String defaultLocale; - - @Autowired - public void configure(MovieFinder movieFinder, - @Value("#{ systemProperties['user.region'] }") String defaultLocale) { - this.movieFinder = movieFinder; - this.defaultLocale = defaultLocale; - } - - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class SimpleMovieLister { - - private lateinit var movieFinder: MovieFinder - private lateinit var defaultLocale: String - - @Autowired - fun configure(movieFinder: MovieFinder, - @Value("#{ systemProperties['user.region'] }") defaultLocale: String) { - this.movieFinder = movieFinder - this.defaultLocale = defaultLocale - } - - // ... - } ----- -====== - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class MovieRecommender { - - private String defaultLocale; - - private CustomerPreferenceDao customerPreferenceDao; - - public MovieRecommender(CustomerPreferenceDao customerPreferenceDao, - @Value("#{systemProperties['user.country']}") String defaultLocale) { - this.customerPreferenceDao = customerPreferenceDao; - this.defaultLocale = defaultLocale; - } - - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class MovieRecommender(private val customerPreferenceDao: CustomerPreferenceDao, - @Value("#{systemProperties['user.country']}") private val defaultLocale: String) { - // ... - } ----- -====== - +include-code::./SimpleMovieLister[tag=snippet,indent=0] +include-code::./MovieRecommender[tag=snippet,indent=0] +You can also refer to other bean properties by name, as the following example shows: +include-code::./ShapeGuess[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc index 2ed0079d67be..bf253821b4e2 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc @@ -1,82 +1,83 @@ [[expressions-evaluation]] = Evaluation -This section introduces the simple use of SpEL interfaces and its expression language. -The complete language reference can be found in +This section introduces programmatic use of SpEL's interfaces and its expression language. +The complete language reference can be found in the xref:core/expressions/language-ref.adoc[Language Reference]. -The following code introduces the SpEL API to evaluate the literal string expression, -`Hello World`. +The following code demonstrates how to use the SpEL API to evaluate the literal string +expression, `Hello World`. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("'Hello World'"); // <1> String message = (String) exp.getValue(); ---- -<1> The value of the message variable is `'Hello World'`. +<1> The value of the message variable is `"Hello World"`. Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val exp = parser.parseExpression("'Hello World'") // <1> val message = exp.value as String ---- -<1> The value of the message variable is `'Hello World'`. +<1> The value of the message variable is `"Hello World"`. ====== - The SpEL classes and interfaces you are most likely to use are located in the `org.springframework.expression` package and its sub-packages, such as `spel.support`. -The `ExpressionParser` interface is responsible for parsing an expression string. In -the preceding example, the expression string is a string literal denoted by the surrounding single -quotation marks. The `Expression` interface is responsible for evaluating the previously defined -expression string. Two exceptions that can be thrown, `ParseException` and -`EvaluationException`, when calling `parser.parseExpression` and `exp.getValue`, -respectively. +The `ExpressionParser` interface is responsible for parsing an expression string. In the +preceding example, the expression string is a string literal denoted by the surrounding +single quotation marks. The `Expression` interface is responsible for evaluating the +defined expression string. The two types of exceptions that can be thrown when calling +`parser.parseExpression(...)` and `exp.getValue(...)` are `ParseException` and +`EvaluationException`, respectively. -SpEL supports a wide range of features, such as calling methods, accessing properties, +SpEL supports a wide range of features such as calling methods, accessing properties, and calling constructors. -In the following example of method invocation, we call the `concat` method on the string literal: +In the following method invocation example, we call the `concat` method on the string +literal, `Hello World`. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("'Hello World'.concat('!')"); // <1> String message = (String) exp.getValue(); ---- -<1> The value of `message` is now 'Hello World!'. +<1> The value of `message` is now `"Hello World!"`. Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val exp = parser.parseExpression("'Hello World'.concat('!')") // <1> val message = exp.value as String ---- -<1> The value of `message` is now 'Hello World!'. +<1> The value of `message` is now `"Hello World!"`. ====== -The following example of calling a JavaBean property calls the `String` property `Bytes`: +The following example demonstrates how to access the `Bytes` JavaBean property of the +string literal, `Hello World`. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); @@ -88,7 +89,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() @@ -100,16 +101,16 @@ Kotlin:: ====== SpEL also supports nested properties by using the standard dot notation (such as -`prop1.prop2.prop3`) and also the corresponding setting of property values. +`prop1.prop2.prop3`) as well as the corresponding setting of property values. Public fields may also be accessed. -The following example shows how to use dot notation to get the length of a literal: +The following example shows how to use dot notation to get the length of a string literal. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); @@ -121,7 +122,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() @@ -133,32 +134,31 @@ Kotlin:: ====== The String's constructor can be called instead of using a string literal, as the following -example shows: +example shows. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); // <1> String message = exp.getValue(String.class); ---- -<1> Construct a new `String` from the literal and make it be upper case. +<1> Construct a new `String` from the literal and convert it to upper case. Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val exp = parser.parseExpression("new String('hello world').toUpperCase()") // <1> val message = exp.getValue(String::class.java) ---- -<1> Construct a new `String` from the literal and make it be upper case. +<1> Construct a new `String` from the literal and convert it to upper case. ====== - Note the use of the generic method: `public T getValue(Class desiredResultType)`. Using this method removes the need to cast the value of the expression to the desired result type. An `EvaluationException` is thrown if the value cannot be cast to the @@ -166,14 +166,14 @@ type `T` or converted by using the registered type converter. The more common usage of SpEL is to provide an expression string that is evaluated against a specific object instance (called the root object). The following example shows -how to retrieve the `name` property from an instance of the `Inventor` class or -create a boolean condition: +how to retrieve the `name` property from an instance of the `Inventor` class and how to +reference the `name` property in a boolean expression. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Create and set a calendar GregorianCalendar c = new GregorianCalendar(); @@ -195,7 +195,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Create and set a calendar val c = GregorianCalendar() @@ -222,29 +222,38 @@ Kotlin:: [[expressions-evaluation-context]] == Understanding `EvaluationContext` -The `EvaluationContext` interface is used when evaluating an expression to resolve -properties, methods, or fields and to help perform type conversion. Spring provides two +The `EvaluationContext` API is used when evaluating an expression to resolve properties, +methods, or fields and to help perform type conversion. Spring provides two implementations. -* `SimpleEvaluationContext`: Exposes a subset of essential SpEL language features and -configuration options, for categories of expressions that do not require the full extent -of the SpEL language syntax and should be meaningfully restricted. Examples include but -are not limited to data binding expressions and property-based filters. +`SimpleEvaluationContext`:: + Exposes a subset of essential SpEL language features and configuration options, for + categories of expressions that do not require the full extent of the SpEL language + syntax and should be meaningfully restricted. Examples include but are not limited to + data binding expressions and property-based filters. + +`StandardEvaluationContext`:: + Exposes the full set of SpEL language features and configuration options. You can use + it to specify a default root object and to configure every available evaluation-related + strategy. -* `StandardEvaluationContext`: Exposes the full set of SpEL language features and -configuration options. You can use it to specify a default root object and to configure -every available evaluation-related strategy. +`SimpleEvaluationContext` is designed to support only a subset of the SpEL language +syntax. For example, it excludes Java type references, constructors, and bean references. +It also requires you to explicitly choose the level of support for properties and methods +in expressions. When creating a `SimpleEvaluationContext` you need to choose the level of +support that you need for data binding in SpEL expressions: -`SimpleEvaluationContext` is designed to support only a subset of the SpEL language syntax. -It excludes Java type references, constructors, and bean references. It also requires -you to explicitly choose the level of support for properties and methods in expressions. -By default, the `create()` static factory method enables only read access to properties. -You can also obtain a builder to configure the exact level of support needed, targeting -one or some combination of the following: +* Data binding for read-only access +* Data binding for read and write access +* A custom `PropertyAccessor` (typically not reflection-based), potentially combined with + a `DataBindingPropertyAccessor` -* Custom `PropertyAccessor` only (no reflection) -* Data binding properties for read-only access -* Data binding properties for read and write +Conveniently, `SimpleEvaluationContext.forReadOnlyDataBinding()` enables read-only access +to properties via `DataBindingPropertyAccessor`. Similarly, +`SimpleEvaluationContext.forReadWriteDataBinding()` enables read and write access to +properties. Alternatively, configure custom accessors via +`SimpleEvaluationContext.forPropertyAccessors(...)`, potentially disable assignment, and +optionally activate method resolution and/or a type converter through the builder. [[expressions-type-conversion]] @@ -252,22 +261,21 @@ one or some combination of the following: By default, SpEL uses the conversion service available in Spring core (`org.springframework.core.convert.ConversionService`). This conversion service comes -with many built-in converters for common conversions but is also fully extensible so that -you can add custom conversions between types. Additionally, it is -generics-aware. This means that, when you work with generic types in -expressions, SpEL attempts conversions to maintain type correctness for any objects -it encounters. +with many built-in converters for common conversions, but is also fully extensible so +that you can add custom conversions between types. Additionally, it is generics-aware. +This means that, when you work with generic types in expressions, SpEL attempts +conversions to maintain type correctness for any objects it encounters. What does this mean in practice? Suppose assignment, using `setValue()`, is being used to set a `List` property. The type of the property is actually `List`. SpEL recognizes that the elements of the list need to be converted to `Boolean` before -being placed in it. The following example shows how to do so: +being placed in it. The following example shows how to do so. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class Simple { public List booleanList = new ArrayList<>(); @@ -288,7 +296,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Simple { var booleanList: MutableList = ArrayList() @@ -315,23 +323,23 @@ Kotlin:: It is possible to configure the SpEL expression parser by using a parser configuration object (`org.springframework.expression.spel.SpelParserConfiguration`). The configuration object controls the behavior of some of the expression components. For example, if you -index into an array or collection and the element at the specified index is `null`, SpEL -can automatically create the element. This is useful when using expressions made up of a -chain of property references. If you index into an array or list and specify an index -that is beyond the end of the current size of the array or list, SpEL can automatically -grow the array or list to accommodate that index. In order to add an element at the +index into a collection and the element at the specified index is `null`, SpEL can +automatically create the element. This is useful when using expressions made up of a +chain of property references. Similarly, if you index into a collection and specify an +index that is greater than the current size of the collection, SpEL can automatically +grow the collection to accommodate that index. In order to add an element at the specified index, SpEL will try to create the element using the element type's default constructor before setting the specified value. If the element type does not have a -default constructor, `null` will be added to the array or list. If there is no built-in -or custom converter that knows how to set the value, `null` will remain in the array or -list at the specified index. The following example demonstrates how to automatically grow -the list: +default constructor, `null` will be added to the collection. If there is no built-in +converter or custom converter that knows how to set the value, `null` will remain in the +collection at the specified index. The following example demonstrates how to +automatically grow a `List`. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class Demo { public List list; @@ -356,7 +364,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Demo { var list: List? = null @@ -380,16 +388,25 @@ Kotlin:: ---- ====== +By default, a SpEL expression cannot contain more than 10,000 characters; however, the +`maxExpressionLength` is configurable. If you create a `SpelExpressionParser` +programmatically, you can specify a custom `maxExpressionLength` when creating the +`SpelParserConfiguration` that you provide to the `SpelExpressionParser`. If you wish to +set the `maxExpressionLength` used for parsing SpEL expressions within an +`ApplicationContext` -- for example, in XML bean definitions, `@Value`, etc. -- you can +set a JVM system property or Spring property named `spring.context.expression.maxLength` +to the maximum expression length needed by your application (see +xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]). [[expressions-spel-compilation]] == SpEL Compilation -Spring Framework 4.1 includes a basic expression compiler. Expressions are usually -interpreted, which provides a lot of dynamic flexibility during evaluation but -does not provide optimum performance. For occasional expression usage, -this is fine, but, when used by other components such as Spring Integration, -performance can be very important, and there is no real need for the dynamism. +Spring provides a basic compiler for SpEL expressions. Expressions are usually +interpreted, which provides a lot of dynamic flexibility during evaluation but does not +provide optimum performance. For occasional expression usage, this is fine, but, when +used by other components such as Spring Integration, performance can be very important, +and there is no real need for the dynamism. The SpEL compiler is intended to address this need. During evaluation, the compiler generates a Java class that embodies the expression behavior at runtime and uses that @@ -402,16 +419,17 @@ information can cause trouble later if the types of the various expression eleme change over time. For this reason, compilation is best suited to expressions whose type information is not going to change on repeated evaluations. -Consider the following basic expression: +Consider the following basic expression. +[source,java,indent=0,subs="verbatim,quotes"] ---- -someArray[0].someProperty.someOtherProperty < 0.1 + someArray[0].someProperty.someOtherProperty < 0.1 ---- -Because the preceding expression involves array access, some property de-referencing, -and numeric operations, the performance gain can be very noticeable. In an example -micro benchmark run of 50000 iterations, it took 75ms to evaluate by using the -interpreter and only 3ms using the compiled version of the expression. +Because the preceding expression involves array access, some property de-referencing, and +numeric operations, the performance gain can be very noticeable. In an example micro +benchmark run of 50,000 iterations, it took 75ms to evaluate by using the interpreter and +only 3ms using the compiled version of the expression. [[expressions-compiler-configuration]] @@ -419,39 +437,49 @@ interpreter and only 3ms using the compiled version of the expression. The compiler is not turned on by default, but you can turn it on in either of two different ways. You can turn it on by using the parser configuration process -(xref:core/expressions/evaluation.adoc#expressions-parser-configuration[discussed earlier]) or by using a Spring property -when SpEL usage is embedded inside another component. This section discusses both of -these options. +(xref:core/expressions/evaluation.adoc#expressions-parser-configuration[discussed +earlier]) or by using a Spring property when SpEL usage is embedded inside another +component. This section discusses both of these options. The compiler can operate in one of three modes, which are captured in the -`org.springframework.expression.spel.SpelCompilerMode` enum. The modes are as follows: - -* `OFF` (default): The compiler is switched off. -* `IMMEDIATE`: In immediate mode, the expressions are compiled as soon as possible. This -is typically after the first interpreted evaluation. If the compiled expression fails -(typically due to a type changing, as described earlier), the caller of the expression -evaluation receives an exception. -* `MIXED`: In mixed mode, the expressions silently switch between interpreted and compiled -mode over time. After some number of interpreted runs, they switch to compiled -form and, if something goes wrong with the compiled form (such as a type changing, as -described earlier), the expression automatically switches back to interpreted form -again. Sometime later, it may generate another compiled form and switch to it. Basically, -the exception that the user gets in `IMMEDIATE` mode is instead handled internally. +`org.springframework.expression.spel.SpelCompilerMode` enum. The modes are as follows. + +`OFF` :: + The compiler is switched off, and all expressions will be evaluated in _interpreted_ + mode. This is the default mode. +`IMMEDIATE` :: + In immediate mode, expressions are compiled as soon as possible, typically after the + first interpreted evaluation. If evaluation of the compiled expression fails (for + example, due to a type changing, as described earlier), the caller of the expression + evaluation receives an exception. If the types of various expression elements change + over time, consider switching to `MIXED` mode or turning off the compiler. +`MIXED` :: + In mixed mode, expression evaluation silently switches between _interpreted_ and + _compiled_ over time. After some number of successful interpreted runs, the expression + gets compiled. If evaluation of the compiled expression fails (for example, due to a + type changing), that failure will be caught internally, and the system will switch back + to interpreted mode for the given expression. Basically, the exception that the caller + receives in `IMMEDIATE` mode is instead handled internally. Sometime later, the + compiler may generate another compiled form and switch to it. This cycle of switching + between interpreted and compiled mode will continue until the system determines that it + does not make sense to continue trying — for example, when a certain failure threshold + has been reached — at which point the system will permanently switch to interpreted + mode for the given expression. `IMMEDIATE` mode exists because `MIXED` mode could cause issues for expressions that have side effects. If a compiled expression blows up after partially succeeding, it may have already done something that has affected the state of the system. If this has happened, the caller may not want it to silently re-run in interpreted mode, -since part of the expression may be running twice. +since part of the expression may be run twice. After selecting a mode, use the `SpelParserConfiguration` to configure the parser. The -following example shows how to do so: +following example shows how to do so. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, this.getClass().getClassLoader()); @@ -467,7 +495,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, this.javaClass.classLoader) @@ -482,15 +510,16 @@ Kotlin:: ---- ====== -When you specify the compiler mode, you can also specify a classloader (passing null is allowed). -Compiled expressions are defined in a child classloader created under any that is supplied. -It is important to ensure that, if a classloader is specified, it can see all the types involved in -the expression evaluation process. If you do not specify a classloader, a default classloader is used -(typically the context classloader for the thread that is running during expression evaluation). +When you specify the compiler mode, you can also specify a `ClassLoader` (passing `null` +is allowed). Compiled expressions are defined in a child `ClassLoader` created under any +that is supplied. It is important to ensure that, if a `ClassLoader` is specified, it can +see all the types involved in the expression evaluation process. If you do not specify a +`ClassLoader`, a default `ClassLoader` is used (typically the context `ClassLoader` for +the thread that is running during expression evaluation). The second way to configure the compiler is for use when SpEL is embedded inside some other component and it may not be possible to configure it through a configuration -object. In these cases, it is possible to set the `spring.expression.compiler.mode` +object. In such cases, it is possible to set the `spring.expression.compiler.mode` property via a JVM system property (or via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism) to one of the `SpelCompilerMode` enum values (`off`, `immediate`, or `mixed`). @@ -499,18 +528,17 @@ xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism) to [[expressions-compiler-limitations]] === Compiler Limitations -Since Spring Framework 4.1, the basic compilation framework is in place. However, the framework -does not yet support compiling every kind of expression. The initial focus has been on the -common expressions that are likely to be used in performance-critical contexts. The following -kinds of expression cannot be compiled at the moment: +Spring does not support compiling every kind of expression. The primary focus is on +common expressions that are likely to be used in performance-critical contexts. The +following kinds of expressions cannot be compiled. * Expressions involving assignment * Expressions relying on the conversion service -* Expressions using custom resolvers or accessors +* Expressions using custom resolvers +* Expressions using overloaded operators +* Expressions using array construction syntax * Expressions using selection or projection +* Expressions using bean references -More types of expressions will be compilable in the future. - - - +Compilation of additional kinds of expressions may be supported in the future. diff --git a/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc b/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc index e94e3b8c091c..2e63732cee1f 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc @@ -3,11 +3,13 @@ This section lists the classes used in the examples throughout this chapter. +== `Inventor` + [tabs] ====== -Inventor.Java:: +Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.spring.samples.spel.inventor; @@ -80,9 +82,9 @@ Inventor.Java:: } ---- -Inventor.kt:: +Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.spring.samples.spel.inventor @@ -95,11 +97,13 @@ Inventor.kt:: ---- ====== +== `PlaceOfBirth` + [tabs] ====== -PlaceOfBirth.java:: +Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.spring.samples.spel.inventor; @@ -135,9 +139,9 @@ PlaceOfBirth.java:: } ---- -PlaceOfBirth.kt:: +Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.spring.samples.spel.inventor @@ -145,11 +149,13 @@ PlaceOfBirth.kt:: ---- ====== +== `Society` + [tabs] ====== -Society.java:: +Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.spring.samples.spel.inventor; @@ -192,9 +198,9 @@ Society.java:: } ---- -Society.kt:: +Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.spring.samples.spel.inventor diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref.adoc index c69ffaf6bc1b..ebce4c294cc4 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref.adoc @@ -2,24 +2,4 @@ = Language Reference :page-section-summary-toc: 1 -This section describes how the Spring Expression Language works. It covers the following -topics: - -* xref:core/expressions/language-ref/literal.adoc[Literal Expressions] -* xref:core/expressions/language-ref/properties-arrays.adoc[Properties, Arrays, Lists, Maps, and Indexers] -* xref:core/expressions/language-ref/inline-lists.adoc[Inline Lists] -* xref:core/expressions/language-ref/inline-maps.adoc[Inline Maps] -* xref:core/expressions/language-ref/array-construction.adoc[Array Construction] -* xref:core/expressions/language-ref/methods.adoc[Methods] -* xref:core/expressions/language-ref/operators.adoc[Operators] -* xref:core/expressions/language-ref/types.adoc[Types] -* xref:core/expressions/language-ref/constructors.adoc[Constructors] -* xref:core/expressions/language-ref/variables.adoc[Variables] -* xref:core/expressions/language-ref/functions.adoc[User-Defined Functions] -* xref:core/expressions/language-ref/bean-references.adoc[Bean References] -* xref:core/expressions/language-ref/operator-ternary.adoc[Ternary Operator (If-Then-Else)] -* xref:core/expressions/language-ref/operator-elvis.adoc[The Elvis Operator] -* xref:core/expressions/language-ref/operator-safe-navigation.adoc[Safe Navigation Operator] - - - +This section describes how the Spring Expression Language works. diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/array-construction.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/array-construction.adoc index 01e181ae3017..aad70436210d 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/array-construction.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/array-construction.adoc @@ -8,12 +8,12 @@ to have the array populated at construction time. The following example shows ho ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context); // Array with initializer - int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context); + int[] numbers2 = (int[]) parser.parseExpression("new int[] {1, 2, 3}").getValue(context); // Multi dimensional array int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context); @@ -21,19 +21,27 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val numbers1 = parser.parseExpression("new int[4]").getValue(context) as IntArray // Array with initializer - val numbers2 = parser.parseExpression("new int[]{1,2,3}").getValue(context) as IntArray + val numbers2 = parser.parseExpression("new int[] {1, 2, 3}").getValue(context) as IntArray // Multi dimensional array val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- ====== +[NOTE] +==== You cannot currently supply an initializer when you construct a multi-dimensional array. - - - +==== + +[CAUTION] +==== +Any expression that constructs an array – for example, via `new int[4]` or +`new int[] {1, 2, 3}` – cannot be compiled. See +xref:core/expressions/evaluation.adoc#expressions-compiler-limitations[Compiler Limitations] +for details. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/bean-references.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/bean-references.adoc index 82e68876b1f0..1102db79deba 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/bean-references.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/bean-references.adoc @@ -1,65 +1,73 @@ [[expressions-bean-references]] = Bean References -If the evaluation context has been configured with a bean resolver, you can -look up beans from an expression by using the `@` symbol. The following example shows how +If the evaluation context has been configured with a bean resolver, you can look up beans +from an expression by using the `@` symbol as a prefix. The following example shows how to do so: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new MyBeanResolver()); - // This will end up calling resolve(context,"something") on MyBeanResolver during evaluation - Object bean = parser.parseExpression("@something").getValue(context); + // This will end up calling resolve(context, "someBean") on MyBeanResolver + // during evaluation. + Object bean = parser.parseExpression("@someBean").getValue(context); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val context = StandardEvaluationContext() context.setBeanResolver(MyBeanResolver()) - // This will end up calling resolve(context,"something") on MyBeanResolver during evaluation - val bean = parser.parseExpression("@something").getValue(context) + // This will end up calling resolve(context, "someBean") on MyBeanResolver + // during evaluation. + val bean = parser.parseExpression("@someBean").getValue(context) ---- ====== -To access a factory bean itself, you should instead prefix the bean name with an `&` symbol. -The following example shows how to do so: +[NOTE] +==== +If a bean name contains a dot (`.`) or other special characters, you must provide the +name of the bean as a _string literal_ – for example, `@'order.service'`. +==== + +To access a factory bean itself, you should instead prefix the bean name with an `&` +symbol. The following example shows how to do so: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new MyBeanResolver()); - // This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation - Object bean = parser.parseExpression("&foo").getValue(context); + // This will end up calling resolve(context, "&someFactoryBean") on + // MyBeanResolver during evaluation. + Object factoryBean = parser.parseExpression("&someFactoryBean").getValue(context); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val context = StandardEvaluationContext() context.setBeanResolver(MyBeanResolver()) - // This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation - val bean = parser.parseExpression("&foo").getValue(context) + // This will end up calling resolve(context, "&someFactoryBean") on + // MyBeanResolver during evaluation. + val factoryBean = parser.parseExpression("&someFactoryBean").getValue(context) ---- ====== - - diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc index 83f492766dd0..2f4ad28fa10f 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc @@ -4,25 +4,27 @@ Projection lets a collection drive the evaluation of a sub-expression, and the result is a new collection. The syntax for projection is `.![projectionExpression]`. For example, suppose we have a list of inventors but want the list of cities where they were born. -Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +Effectively, we want to evaluate `placeOfBirth.city` for every entry in the inventor list. The following example uses projection to do so: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - // returns ['Smiljan', 'Idvor' ] - List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]"); + // evaluates to ["Smiljan", "Idvor"] + List placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") + .getValue(societyContext, List.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - // returns ['Smiljan', 'Idvor' ] - val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> + // evaluates to ["Smiljan", "Idvor"] + val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") + .getValue(societyContext) as List<*> ---- ====== @@ -32,5 +34,12 @@ evaluated against each entry in the map (represented as a Java `Map.Entry`). The of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. +[NOTE] +==== +The Spring Expression Language also supports safe navigation for collection projection. +See +xref:core/expressions/language-ref/operator-safe-navigation.adoc#expressions-operator-safe-navigation-selection-and-projection[Safe Collection Selection and Projection] +for details. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc index 3f87541a81cb..5f66882d608f 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc @@ -12,7 +12,7 @@ selection lets us easily get a list of Serbian inventors, as the following examp ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- List list = (List) parser.parseExpression( "members.?[nationality == 'Serbian']").getValue(societyContext); @@ -20,7 +20,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val list = parser.parseExpression( "members.?[nationality == 'Serbian']").getValue(societyContext) as List @@ -28,35 +28,43 @@ Kotlin:: ====== Selection is supported for arrays and anything that implements `java.lang.Iterable` or -`java.util.Map`. For a list or array, the selection criteria is evaluated against each -individual element. Against a map, the selection criteria is evaluated against each map -entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` -accessible as properties for use in the selection. +`java.util.Map`. For an array or `Iterable`, the selection expression is evaluated +against each individual element. Against a map, the selection expression is evaluated +against each map entry (objects of the Java type `Map.Entry`). Each map entry has its +`key` and `value` accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the -original map where the entry's value is less than 27: +Given a `Map` stored in a variable named `#map`, the following expression returns a new +map that consists of those elements of the original map where the entry's value is less +than 27: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - Map newMap = parser.parseExpression("map.?[value<27]").getValue(); + Map newMap = parser.parseExpression("#map.?[value < 27]").getValue(Map.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val newMap = parser.parseExpression("map.?[value<27]").getValue() + val newMap = parser.parseExpression("#map.?[value < 27]").getValue() as Map ---- ====== In addition to returning all the selected elements, you can retrieve only the first or -the last element. To obtain the first element matching the selection, the syntax is -`.^[selectionExpression]`. To obtain the last matching selection, the syntax is -`.$[selectionExpression]`. +the last element. To obtain the first element matching the selection expression, the +syntax is `.^[selectionExpression]`. To obtain the last element matching the selection +expression, the syntax is `.$[selectionExpression]`. +[NOTE] +==== +The Spring Expression Language also supports safe navigation for collection selection. +See +xref:core/expressions/language-ref/operator-safe-navigation.adoc#expressions-operator-safe-navigation-selection-and-projection[Safe Collection Selection and Projection] +for details. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc index a35513c9c1ec..4057f7943ac5 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc @@ -3,39 +3,40 @@ You can invoke constructors by using the `new` operator. You should use the fully qualified class name for all types except those located in the `java.lang` package -(`Integer`, `Float`, `String`, and so on). The following example shows how to use the -`new` operator to invoke constructors: +(`Integer`, `Float`, `String`, and so on). +xref:core/expressions/language-ref/varargs.adoc[Varargs] are also supported. + +The following example shows how to use the `new` operator to invoke constructors. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - Inventor einstein = p.parseExpression( - "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") + Inventor einstein = parser.parseExpression( + "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); // create new Inventor instance within the add() method of List - p.parseExpression( - "Members.add(new org.spring.samples.spel.inventor.Inventor( - 'Albert Einstein', 'German'))").getValue(societyContext); + parser.parseExpression( + "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") + .getValue(societyContext); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val einstein = p.parseExpression( - "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") + val einstein = parser.parseExpression( + "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) // create new Inventor instance within the add() method of List - p.parseExpression( - "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") + parser.parseExpression( + "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) ---- ====== - diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc index 4621b2a19aaa..00f3cb56fd35 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc @@ -1,16 +1,36 @@ [[expressions-ref-functions]] = Functions -You can extend SpEL by registering user-defined functions that can be called within the -expression string. The function is registered through the `EvaluationContext`. The -following example shows how to register a user-defined function to be invoked via reflection -(i.e. a `Method`): +You can extend SpEL by registering user-defined functions that can be called within +expressions by using the `#functionName(...)` syntax, and like with standard method +invocations, xref:core/expressions/language-ref/varargs.adoc[varargs] are also supported +for function invocations. + +Functions can be registered as _variables_ in `EvaluationContext` implementations via the +`setVariable()` method. + +[TIP] +==== +`StandardEvaluationContext` also defines `registerFunction(...)` methods that provide a +convenient way to register a function as a `java.lang.reflect.Method` or a +`java.lang.invoke.MethodHandle`. +==== + +[WARNING] +==== +Since functions share a common namespace with +xref:core/expressions/language-ref/variables.adoc[variables] in the evaluation context, +care must be taken to ensure that function names and variable names do not overlap. +==== + +The following example shows how to register a user-defined function to be invoked via +reflection using a `java.lang.reflect.Method`: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Method method = ...; @@ -20,7 +40,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val method: Method = ... @@ -35,72 +55,68 @@ For example, consider the following utility method that reverses a string: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public abstract class StringUtils { public static String reverseString(String input) { - StringBuilder backwards = new StringBuilder(input.length()); - for (int i = 0; i < input.length(); i++) { - backwards.append(input.charAt(input.length() - 1 - i)); - } - return backwards.toString(); + return new StringBuilder(input).reverse().toString(); } } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun reverseString(input: String): String { - val backwards = StringBuilder(input.length) - for (i in 0 until input.length) { - backwards.append(input[input.length - 1 - i]) - } - return backwards.toString() + return StringBuilder(input).reverse().toString() } ---- ====== -You can then register and use the preceding method, as the following example shows: +You can register and use the preceding method, as the following example shows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); context.setVariable("reverseString", - StringUtils.class.getDeclaredMethod("reverseString", String.class)); + StringUtils.class.getMethod("reverseString", String.class)); + // evaluates to "olleh" String helloWorldReversed = parser.parseExpression( "#reverseString('hello')").getValue(context, String.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() - context.setVariable("reverseString", ::reverseString::javaMethod) + context.setVariable("reverseString", ::reverseString.javaMethod) + // evaluates to "olleh" val helloWorldReversed = parser.parseExpression( "#reverseString('hello')").getValue(context, String::class.java) ---- ====== -The use of `MethodHandle` is also supported. This enables potentially more efficient use -cases if the `MethodHandle` target and parameters have been fully bound prior to -registration, but partially bound handles are also supported. +A function can also be registered as a `java.lang.invoke.MethodHandle`. This enables +potentially more efficient use cases if the `MethodHandle` target and parameters have +been fully bound prior to registration; however, partially bound handles are also +supported. -Consider the `String#formatted(String, Object...)` instance method, which produces a -message according to a template and a variable number of arguments. +Consider the `String#formatted(Object...)` instance method, which produces a message +according to a template and a variable number of arguments +(xref:core/expressions/language-ref/varargs.adoc[varargs]). You can register and use the `formatted` method as a `MethodHandle`, as the following example shows: @@ -109,7 +125,7 @@ example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); @@ -118,14 +134,14 @@ Java:: MethodType.methodType(String.class, Object[].class)); context.setVariable("message", mh); + // evaluates to "Simple message: " String message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')") .getValue(context, String.class); - //returns "Simple message: " ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() @@ -134,21 +150,22 @@ Kotlin:: MethodType.methodType(String::class.java, Array::class.java)) context.setVariable("message", mh) + // evaluates to "Simple message: " val message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')") .getValue(context, String::class.java) ---- ====== -As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also -supported. This is likely to be more performant if both the target and all the arguments -are bound. In that case no arguments are necessary in the SpEL expression, as the -following example shows: +As mentioned above, binding a `MethodHandle` and registering the bound `MethodHandle` is +also supported. This is likely to be more performant if both the target and all the +arguments are bound. In that case no arguments are necessary in the SpEL expression, as +the following example shows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); @@ -156,19 +173,20 @@ Java:: String template = "This is a %s message with %s words: <%s>"; Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" }; MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted", - MethodType.methodType(String.class, Object[].class)) + MethodType.methodType(String.class, Object[].class)) .bindTo(template) - .bindTo(varargs); //here we have to provide arguments in a single array binding + // Here we have to provide the arguments in a single array binding: + .bindTo(varargs); context.setVariable("message", mh); + // evaluates to "This is a prerecorded message with 3 words: " String message = parser.parseExpression("#message()") .getValue(context, String.class); - //returns "This is a prerecorded message with 3 words: " ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() @@ -177,15 +195,16 @@ Kotlin:: val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored") val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted", - MethodType.methodType(String::class.java, Array::class.java)) + MethodType.methodType(String::class.java, Array::class.java)) .bindTo(template) - .bindTo(varargs) //here we have to provide arguments in a single array binding + // Here we have to provide the arguments in a single array binding: + .bindTo(varargs) context.setVariable("message", mh) + // evaluates to "This is a prerecorded message with 3 words: " val message = parser.parseExpression("#message()") .getValue(context, String::class.java) ---- ====== - diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/inline-lists.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/inline-lists.adoc index 463d54d80955..5bcea13768fa 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/inline-lists.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/inline-lists.adoc @@ -7,7 +7,7 @@ You can directly express lists in an expression by using `{}` notation. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // evaluates to a Java list containing the four numbers List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context); @@ -17,7 +17,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // evaluates to a Java list containing the four numbers val numbers = parser.parseExpression("{1,2,3,4}").getValue(context) as List<*> diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/inline-maps.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/inline-maps.adoc index 2b972329cd8b..122b3d651202 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/inline-maps.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/inline-maps.adoc @@ -8,7 +8,7 @@ following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // evaluates to a Java map containing the two entries Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context); @@ -18,7 +18,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- // evaluates to a Java map containing the two entries val inventorInfo = parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context) as Map<*, *> diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc index c133af0f367a..a191ed6f9354 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/literal.adoc @@ -3,20 +3,46 @@ SpEL supports the following types of literal expressions. -- strings -- numeric values: integer (`int` or `long`), hexadecimal (`int` or `long`), real (`float` - or `double`) -- boolean values: `true` or `false` -- null - -Strings can delimited by single quotation marks (`'`) or double quotation marks (`"`). To -include a single quotation mark within a string literal enclosed in single quotation -marks, use two adjacent single quotation mark characters. Similarly, to include a double -quotation mark within a string literal enclosed in double quotation marks, use two -adjacent double quotation mark characters. - -Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using `Double.parseDouble()`. +String :: + Strings can be delimited by single quotation marks (`'`) or double quotation marks + (`"`). To include a single quotation mark within a string literal enclosed in single + quotation marks, use two adjacent single quotation mark characters. Similarly, to + include a double quotation mark within a string literal enclosed in double quotation + marks, use two adjacent double quotation mark characters. +Number :: + Numbers support the use of the negative sign, exponential notation, and decimal points. + * Integer: `int` or `long` + * Hexadecimal: `int` or `long` + * Real: `float` or `double` + ** By default, real numbers are parsed using `Double.parseDouble()`. +Boolean :: + `true` or `false` +Null :: + `null` + +[NOTE] +==== +Due to the design and implementation of the Spring Expression Language, literal numbers +are always stored internally as positive numbers. + +For example, `-2` is stored internally as a positive `2` which is then negated while +evaluating the expression (by calculating the value of `0 - 2`). + +This means that it is not possible to represent a negative literal number equal to the +minimum value of that type of number in Java. For example, the minimum supported value +for an `int` in Java is `Integer.MIN_VALUE` which has a value of `-2147483648`. However, +if you include `-2147483648` in a SpEL expression, an exception will be thrown informing +you that the value `2147483648` cannot be parsed as an `int` (because it exceeds the +value of `Integer.MAX_VALUE` which is `2147483647`). + +If you need to use the minimum value for a particular type of number within a SpEL +expression, you can either reference the `MIN_VALUE` constant for the respective wrapper +type (such as `Integer.MIN_VALUE`, `Long.MIN_VALUE`, etc.) or calculate the minimum +value. For example, to use the minimum integer value: + +- `T(Integer).MIN_VALUE` -- requires a `StandardEvaluationContext` +- `-2^31` -- can be used with any type of `EvaluationContext` +==== The following listing shows simple usage of literals. Typically, they are not used in isolation like this but, rather, as part of a more complex expression -- for example, @@ -27,7 +53,7 @@ method. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); @@ -49,7 +75,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/methods.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/methods.adoc index 46c91b836254..6e6f26e1737b 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/methods.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/methods.adoc @@ -1,15 +1,17 @@ [[expressions-methods]] = Methods -You can invoke methods by using typical Java programming syntax. You can also invoke methods -on literals. Variable arguments are also supported. The following examples show how to -invoke methods: +You can invoke methods by using the typical Java programming syntax. You can also invoke +methods directly on literals such as strings or numbers. +xref:core/expressions/language-ref/varargs.adoc[Varargs] are supported as well. + +The following examples show how to invoke methods. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // string literal, evaluates to "bc" String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class); @@ -21,7 +23,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // string literal, evaluates to "bc" val bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String::class.java) 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 3884dcadf2f7..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,46 +1,61 @@ [[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] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- 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' ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- 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: @@ -48,38 +63,38 @@ The following listing shows a more complex example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); 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 ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() 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 0691cf2de87a..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 @@ -1,18 +1,33 @@ [[expressions-operator-safe-navigation]] = Safe Navigation Operator -The safe navigation operator is used to avoid a `NullPointerException` and comes from -the https://www.groovy-lang.org/operators.html#_safe_navigation_operator[Groovy] -language. Typically, when you have a reference to an object, you might need to verify that -it is not null before accessing methods or properties of the object. To avoid this, the -safe navigation operator returns null instead of throwing an exception. The following -example shows how to use the safe navigation operator: +The safe navigation operator (`?.`) is used to avoid a `NullPointerException` and comes +from the https://www.groovy-lang.org/operators.html#_safe_navigation_operator[Groovy] +language. Typically, when you have a reference to an object, you might need to verify +that it is not `null` before accessing methods or properties of the object. To avoid +this, the safe navigation operator returns `null` for the particular null-safe operation +instead of throwing an exception. + +[WARNING] +==== +When the safe navigation operator evaluates to `null` for a particular null-safe +operation within a compound expression, the remainder of the compound expression will +still be evaluated. + +See <> for details. +==== + +[[expressions-operator-safe-navigation-property-access]] +== Safe Property and Method Access + +The following example shows how to use the safe navigation operator for property access +(`?.`). [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); @@ -20,17 +35,22 @@ Java:: Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan")); - String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); - System.out.println(city); // Smiljan + // evaluates to "Smiljan" + String city = parser.parseExpression("placeOfBirth?.city") // <1> + .getValue(context, tesla, String.class); tesla.setPlaceOfBirth(null); - city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); - System.out.println(city); // null - does not throw NullPointerException!!! + + // evaluates to null - does not throw NullPointerException + city = parser.parseExpression("placeOfBirth?.city") // <2> + .getValue(context, tesla, String.class); ---- +<1> Use safe navigation operator on non-null `placeOfBirth` property +<2> Use safe navigation operator on null `placeOfBirth` property Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() @@ -38,14 +58,408 @@ Kotlin:: val tesla = Inventor("Nikola Tesla", "Serbian") tesla.setPlaceOfBirth(PlaceOfBirth("Smiljan")) - var city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java) - println(city) // Smiljan + // evaluates to "Smiljan" + var city = parser.parseExpression("placeOfBirth?.city") // <1> + .getValue(context, tesla, String::class.java) tesla.setPlaceOfBirth(null) - city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java) - println(city) // null - does not throw NullPointerException!!! + + // evaluates to null - does not throw NullPointerException + city = parser.parseExpression("placeOfBirth?.city") // <2> + .getValue(context, tesla, String::class.java) +---- +<1> Use safe navigation operator on non-null `placeOfBirth` property +<2> Use safe navigation operator on null `placeOfBirth` property +====== + +[NOTE] +==== +The safe navigation operator also applies to method invocations on an object. + +For example, the expression `#calculator?.max(4, 2)` evaluates to `null` if the +`#calculator` variable has not been configured in the context. Otherwise, the +`max(int, int)` method will be invoked on the `#calculator`. +==== + +[[expressions-operator-safe-navigation-indexing]] +== Safe Index Access + +Since Spring Framework 6.2, the Spring Expression Language supports safe navigation for +indexing into the following types of structures. + +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-arrays-and-collections[arrays and collections] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-strings[strings] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-maps[maps] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-objects[objects] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-custom[custom] + +The following example shows how to use the safe navigation operator for indexing into +a list (`?.[]`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + EvaluationContext context = new StandardEvaluationContext(society); + + // evaluates to Inventor("Nikola Tesla") + Inventor inventor = parser.parseExpression("members?.[0]") // <1> + .getValue(context, Inventor.class); + + society.members = null; + + // evaluates to null - does not throw an exception + inventor = parser.parseExpression("members?.[0]") // <2> + .getValue(context, Inventor.class); +---- +<1> Use null-safe index operator on a non-null `members` list +<2> Use null-safe index operator on a null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + + // evaluates to Inventor("Nikola Tesla") + var inventor = parser.parseExpression("members?.[0]") // <1> + .getValue(context, Inventor::class.java) + + society.members = null + + // evaluates to null - does not throw an exception + inventor = parser.parseExpression("members?.[0]") // <2> + .getValue(context, Inventor::class.java) +---- +<1> Use null-safe index operator on a non-null `members` list +<2> Use null-safe index operator on a null `members` list +====== + +[[expressions-operator-safe-navigation-selection-and-projection]] +== Safe Collection Selection and Projection + +The Spring Expression Language supports safe navigation for +xref:core/expressions/language-ref/collection-selection.adoc[collection selection] and +xref:core/expressions/language-ref/collection-projection.adoc[collection projection] via +the following operators. + +* null-safe selection: `?.?` +* null-safe select first: `?.^` +* null-safe select last: `?.$` +* null-safe projection: `?.!` + +The following example shows how to use the safe navigation operator for collection +selection (`?.?`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = "members?.?[nationality == 'Serbian']"; // <1> + + // evaluates to [Inventor("Nikola Tesla")] + List list = (List) parser.parseExpression(expression) + .getValue(context); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + list = (List) parser.parseExpression(expression) + .getValue(context); +---- +<1> Use null-safe selection operator on potentially null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + val expression = "members?.?[nationality == 'Serbian']" // <1> + + // evaluates to [Inventor("Nikola Tesla")] + var list = parser.parseExpression(expression) + .getValue(context) as List + + society.members = null + + // evaluates to null - does not throw a NullPointerException + list = parser.parseExpression(expression) + .getValue(context) as List +---- +<1> Use null-safe selection operator on potentially null `members` list +====== + +The following example shows how to use the "null-safe select first" operator for +collections (`?.^`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = + "members?.^[nationality == 'Serbian' || nationality == 'Idvor']"; // <1> + + // evaluates to Inventor("Nikola Tesla") + Inventor inventor = parser.parseExpression(expression) + .getValue(context, Inventor.class); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + inventor = parser.parseExpression(expression) + .getValue(context, Inventor.class); +---- +<1> Use "null-safe select first" operator on potentially null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + val expression = + "members?.^[nationality == 'Serbian' || nationality == 'Idvor']" // <1> + + // evaluates to Inventor("Nikola Tesla") + var inventor = parser.parseExpression(expression) + .getValue(context, Inventor::class.java) + + society.members = null + + // evaluates to null - does not throw a NullPointerException + inventor = parser.parseExpression(expression) + .getValue(context, Inventor::class.java) +---- +<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 (`?.$`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = + "members?.$[nationality == 'Serbian' || nationality == 'Idvor']"; // <1> + + // evaluates to Inventor("Pupin") + Inventor inventor = parser.parseExpression(expression) + .getValue(context, Inventor.class); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + inventor = parser.parseExpression(expression) + .getValue(context, Inventor.class); +---- +<1> Use "null-safe select last" operator on potentially null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + val expression = + "members?.$[nationality == 'Serbian' || nationality == 'Idvor']" // <1> + + // evaluates to Inventor("Pupin") + var inventor = parser.parseExpression(expression) + .getValue(context, Inventor::class.java) + + society.members = null + + // evaluates to null - does not throw a NullPointerException + inventor = parser.parseExpression(expression) + .getValue(context, Inventor::class.java) +---- +<1> Use "null-safe select last" operator on potentially null `members` list +====== + +The following example shows how to use the safe navigation operator for collection +projection (`?.!`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + + // evaluates to ["Smiljan", "Idvor"] + List placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1> + .getValue(context, List.class); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2> + .getValue(context, List.class); +---- +<1> Use null-safe projection operator on non-null `members` list +<2> Use null-safe projection operator on null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + + // evaluates to ["Smiljan", "Idvor"] + var placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <1> + .getValue(context, List::class.java) + + society.members = null + + // evaluates to null - does not throw a NullPointerException + placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") // <2> + .getValue(context, List::class.java) ---- +<1> Use null-safe projection operator on non-null `members` list +<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 + +As mentioned at the beginning of this section, when the safe navigation operator +evaluates to `null` for a particular null-safe operation within a compound expression, +the remainder of the compound expression will still be evaluated. This means that the +safe navigation operator must be applied throughout a compound expression in order to +avoid any unwanted `NullPointerException`. + +Given the expression `#person?.address.city`, if `#person` is `null` the safe navigation +operator (`?.`) ensures that no exception will be thrown when attempting to access the +`address` property of `#person`. However, since `#person?.address` evaluates to `null`, a +`NullPointerException` will be thrown when attempting to access the `city` property of +`null`. To address that, you can apply null-safe navigation throughout the compound +expression as in `#person?.address?.city`. That expression will safely evaluate to `null` +if either `#person` or `#person?.address` evaluates to `null`. + +The following example demonstrates how to use the "null-safe select first" operator +(`?.^`) on a collection combined with null-safe property access (`?.`) within a compound +expression. If `members` is `null`, the result of the "null-safe select first" operator +(`members?.^[nationality == 'Serbian']`) evaluates to `null`, and the additional use of +the safe navigation operator (`?.name`) ensures that the entire compound expression +evaluates to `null` instead of throwing an exception. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = "members?.^[nationality == 'Serbian']?.name"; // <1> + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression(expression) + .getValue(context, String.class); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + name = parser.parseExpression(expression) + .getValue(context, String.class); +---- +<1> Use "null-safe select first" and null-safe property access operators within compound expression. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + val expression = "members?.^[nationality == 'Serbian']?.name" // <1> + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression(expression) + .getValue(context, String::class.java) + + society.members = null + + // evaluates to null - does not throw a NullPointerException + name = parser.parseExpression(expression) + .getValue(context, String::class.java) +---- +<1> Use "null-safe select first" and null-safe property access operators within compound expression. +====== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-ternary.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-ternary.adoc index 0a834d195fd6..09defa169927 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-ternary.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-ternary.adoc @@ -8,7 +8,7 @@ the expression. The following listing shows a minimal example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String falseString = parser.parseExpression( "false ? 'trueExp' : 'falseExp'").getValue(String.class); @@ -16,7 +16,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val falseString = parser.parseExpression( "false ? 'trueExp' : 'falseExp'").getValue(String::class.java) @@ -30,7 +30,7 @@ realistic example follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- parser.parseExpression("name").setValue(societyContext, "IEEE"); societyContext.setVariable("queryName", "Nikola Tesla"); @@ -45,7 +45,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- parser.parseExpression("name").setValue(societyContext, "IEEE") societyContext.setVariable("queryName", "Nikola Tesla") diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc index 7d9d6ece8e4e..92c0b3db563a 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc @@ -5,8 +5,11 @@ The Spring Expression Language supports the following kinds of operators: * xref:core/expressions/language-ref/operators.adoc#expressions-operators-relational[Relational Operators] * xref:core/expressions/language-ref/operators.adoc#expressions-operators-logical[Logical Operators] +* xref:core/expressions/language-ref/operators.adoc#expressions-operators-string[String Operators] * xref:core/expressions/language-ref/operators.adoc#expressions-operators-mathematical[Mathematical Operators] * xref:core/expressions/language-ref/operators.adoc#expressions-assignment[The Assignment Operator] +* xref:core/expressions/language-ref/operators.adoc#expressions-operators-overloaded[Overloaded Operators] + [[expressions-operators-relational]] @@ -15,13 +18,13 @@ The Spring Expression Language supports the following kinds of operators: The relational operators (equal, not equal, less than, less than or equal, greater than, and greater than or equal) are supported by using standard operator notation. These operators work on `Number` types as well as types implementing `Comparable`. -The following listing shows a few examples of operators: +The following listing shows a few examples of relational operators: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // evaluates to true boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class); @@ -38,7 +41,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // evaluates to true val trueValue = parser.parseExpression("2 == 2").getValue(Boolean::class.java) @@ -65,83 +68,134 @@ If you prefer numeric comparisons instead, avoid number-based `null` comparisons in favor of comparisons against zero (for example, `X > 0` or `X < 0`). ==== -In addition to the standard relational operators, SpEL supports the `instanceof` and regular -expression-based `matches` operator. The following listing shows examples of both: +Each symbolic operator can also be specified as a purely textual equivalent. This avoids +problems where the symbols used have special meaning for the document type in which the +expression is embedded (such as in an XML document). The textual equivalents are: + +* `lt` (`<`) +* `gt` (`>`) +* `le` (`\<=`) +* `ge` (`>=`) +* `eq` (`==`) +* `ne` (`!=`) + +All of the textual operators are case-insensitive. + +In addition to the standard relational operators, SpEL supports the `between`, +`instanceof`, and regular expression-based `matches` operators. The following listing +shows examples of all three: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- + boolean result; + + // evaluates to true + result = parser.parseExpression( + "1 between {1, 5}").getValue(Boolean.class); + + // evaluates to false + result = parser.parseExpression( + "1 between {10, 15}").getValue(Boolean.class); + + // evaluates to true + result = parser.parseExpression( + "'elephant' between {'aardvark', 'zebra'}").getValue(Boolean.class); + + // evaluates to false + result = parser.parseExpression( + "'elephant' between {'aardvark', 'cobra'}").getValue(Boolean.class); + + // evaluates to true + result = parser.parseExpression( + "123 instanceof T(Integer)").getValue(Boolean.class); + // evaluates to false - boolean falseValue = parser.parseExpression( + result = parser.parseExpression( "'xyz' instanceof T(Integer)").getValue(Boolean.class); // evaluates to true - boolean trueValue = parser.parseExpression( + result = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); // evaluates to false - boolean falseValue = parser.parseExpression( + result = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- + // evaluates to true + var result = parser.parseExpression( + "1 between {1, 5}").getValue(Boolean::class.java) + + // evaluates to false + result = parser.parseExpression( + "1 between {10, 15}").getValue(Boolean::class.java) + + // evaluates to true + result = parser.parseExpression( + "'elephant' between {'aardvark', 'zebra'}").getValue(Boolean::class.java) + + // evaluates to false + result = parser.parseExpression( + "'elephant' between {'aardvark', 'cobra'}").getValue(Boolean::class.java) + + // evaluates to true + result = parser.parseExpression( + "123 instanceof T(Integer)").getValue(Boolean::class.java) + // evaluates to false - val falseValue = parser.parseExpression( + result = parser.parseExpression( "'xyz' instanceof T(Integer)").getValue(Boolean::class.java) // evaluates to true - val trueValue = parser.parseExpression( + result = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) // evaluates to false - val falseValue = parser.parseExpression( + result = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- ====== -CAUTION: Be careful with primitive types, as they are immediately boxed up to their -wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while -`1 instanceof T(Integer)` evaluates to `true`, as expected. - -Each symbolic operator can also be specified as a purely alphabetic equivalent. This -avoids problems where the symbols used have special meaning for the document type in -which the expression is embedded (such as in an XML document). The textual equivalents are: +[CAUTION] +==== +The syntax for the `between` operator is ` between {, }`, +which is effectively a shortcut for ` >= && \<= }`. -* `lt` (`<`) -* `gt` (`>`) -* `le` (`\<=`) -* `ge` (`>=`) -* `eq` (`==`) -* `ne` (`!=`) -* `div` (`/`) -* `mod` (`%`) -* `not` (`!`). +Consequently, `1 between {1, 5}` evaluates to `true`, while `1 between {5, 1}` evaluates +to `false`. +==== -All of the textual operators are case-insensitive. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`. [[expressions-operators-logical]] == Logical Operators -SpEL supports the following logical operators: +SpEL supports the following logical (`boolean`) operators: * `and` (`&&`) * `or` (`||`) * `not` (`!`) +All of the textual operators are case-insensitive. + The following example shows how to use the logical operators: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // -- AND -- @@ -167,13 +221,14 @@ Java:: boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class); // -- AND and NOT -- + String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')"; boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // -- AND -- @@ -199,88 +254,237 @@ Kotlin:: val falseValue = parser.parseExpression("!true").getValue(Boolean::class.java) // -- AND and NOT -- + val expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')" val falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java) ---- ====== +[[expressions-operators-string]] +== String Operators + +You can use the following operators on strings. + +* concatenation (`+`) +* subtraction (`-`) + - for use with a string containing a single character +* repeat (`*`) + +The following example shows the `String` operators in use: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // -- Concatenation -- + + // evaluates to "hello world" + String helloWorld = parser.parseExpression("'hello' + ' ' + 'world'") + .getValue(String.class); + + // -- Character Subtraction -- + + // evaluates to 'a' + char ch = parser.parseExpression("'d' - 3") + .getValue(char.class); + + // -- Repeat -- + + // evaluates to "abcabc" + String repeated = parser.parseExpression("'abc' * 2") + .getValue(String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // -- Concatenation -- + + // evaluates to "hello world" + val helloWorld = parser.parseExpression("'hello' + ' ' + 'world'") + .getValue(String::class.java) + + // -- Character Subtraction -- + + // evaluates to 'a' + val ch = parser.parseExpression("'d' - 3") + .getValue(Character::class.java); + + // -- Repeat -- + + // evaluates to "abcabc" + val repeated = parser.parseExpression("'abc' * 2") + .getValue(String::class.java); +---- +====== + [[expressions-operators-mathematical]] == Mathematical Operators -You can use the addition operator (`+`) on both numbers and strings. You can use the -subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. -You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. -Standard operator precedence is enforced. The following example shows the mathematical -operators in use: +You can use the following operators on numbers, and standard operator precedence is enforced. + +* addition (`+`) +* subtraction (`-`) +* increment (`{pp}`) +* decrement (`--`) +* multiplication (`*`) +* division (`/`) +* modulus (`%`) +* exponential power (`^`) + +The division and modulus operators can also be specified as a purely textual equivalent. +This avoids problems where the symbols used have special meaning for the document type in +which the expression is embedded (such as in an XML document). The textual equivalents +are: + +* `div` (`/`) +* `mod` (`%`) + +All of the textual operators are case-insensitive. + +[NOTE] +==== +The increment and decrement operators can be used with either prefix (`{pp}A`, `--A`) or +postfix (`A{pp}`, `A--`) notation with variables or properties that can be written to. +==== + +The following example shows the mathematical operators in use: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - // Addition - int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2 + Inventor inventor = new Inventor(); + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + + // -- Addition -- + + int two = parser.parseExpression("1 + 1").getValue(int.class); // 2 + + // -- Subtraction -- + + int four = parser.parseExpression("1 - -3").getValue(int.class); // 4 + + double d = parser.parseExpression("1000.00 - 1e4").getValue(double.class); // -9000 + + // -- Increment -- - String testString = parser.parseExpression( - "'test' + ' ' + 'string'").getValue(String.class); // 'test string' + // The counter property in Inventor has an initial value of 0. - // Subtraction - int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4 + // evaluates to 2; counter is now 1 + two = parser.parseExpression("counter++ + 2").getValue(context, inventor, int.class); - double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000 + // evaluates to 5; counter is now 2 + int five = parser.parseExpression("3 + ++counter").getValue(context, inventor, int.class); - // Multiplication - int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6 + // -- Decrement -- - double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0 + // The counter property in Inventor has a value of 2. - // Division - int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2 + // evaluates to 6; counter is now 1 + int six = parser.parseExpression("counter-- + 4").getValue(context, inventor, int.class); - double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0 + // evaluates to 5; counter is now 0 + five = parser.parseExpression("5 + --counter").getValue(context, inventor, int.class); - // Modulus - int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3 + // -- Multiplication -- - int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1 + six = parser.parseExpression("-2 * -3").getValue(int.class); // 6 - // Operator precedence - int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21 + double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(double.class); // 24.0 + + // -- Division -- + + int minusTwo = parser.parseExpression("6 / -3").getValue(int.class); // -2 + + double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(double.class); // 1.0 + + // -- Modulus -- + + int three = parser.parseExpression("7 % 4").getValue(int.class); // 3 + + int oneInt = parser.parseExpression("8 / 5 % 2").getValue(int.class); // 1 + + // -- Exponential power -- + + int maxInt = parser.parseExpression("(2^31) - 1").getValue(int.class); // Integer.MAX_VALUE + + int minInt = parser.parseExpression("-2^31").getValue(int.class); // Integer.MIN_VALUE + + // -- Operator precedence -- + + int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(int.class); // -21 ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - // Addition - val two = parser.parseExpression("1 + 1").getValue(Int::class.java) // 2 + val inventor = Inventor() + val context = SimpleEvaluationContext.forReadWriteDataBinding().build() + + // -- Addition -- + + var two = parser.parseExpression("1 + 1").getValue(Int::class.java) // 2 - val testString = parser.parseExpression( - "'test' + ' ' + 'string'").getValue(String::class.java) // 'test string' + // -- Subtraction -- - // Subtraction val four = parser.parseExpression("1 - -3").getValue(Int::class.java) // 4 val d = parser.parseExpression("1000.00 - 1e4").getValue(Double::class.java) // -9000 - // Multiplication - val six = parser.parseExpression("-2 * -3").getValue(Int::class.java) // 6 + // -- Increment -- + + // The counter property in Inventor has an initial value of 0. + + // evaluates to 2; counter is now 1 + two = parser.parseExpression("counter++ + 2").getValue(context, inventor, Int::class.java) + + // evaluates to 5; counter is now 2 + var five = parser.parseExpression("3 + ++counter").getValue(context, inventor, Int::class.java) + + // -- Decrement -- + + // The counter property in Inventor has a value of 2. + + // evaluates to 6; counter is now 1 + var six = parser.parseExpression("counter-- + 4").getValue(context, inventor, Int::class.java) + + // evaluates to 5; counter is now 0 + five = parser.parseExpression("5 + --counter").getValue(context, inventor, Int::class.java) + + // -- Multiplication -- + + six = parser.parseExpression("-2 * -3").getValue(Int::class.java) // 6 val twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double::class.java) // 24.0 - // Division + // -- Division -- + val minusTwo = parser.parseExpression("6 / -3").getValue(Int::class.java) // -2 val one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double::class.java) // 1.0 - // Modulus + // -- Modulus -- + val three = parser.parseExpression("7 % 4").getValue(Int::class.java) // 3 - val one = parser.parseExpression("8 / 5 % 2").getValue(Int::class.java) // 1 + val oneInt = parser.parseExpression("8 / 5 % 2").getValue(Int::class.java) // 1 + + // -- Exponential power -- + + val maxInt = parser.parseExpression("(2^31) - 1").getValue(Int::class.java) // Integer.MAX_VALUE + + val minInt = parser.parseExpression("-2^31").getValue(Int::class.java) // Integer.MIN_VALUE + + // -- Operator precedence -- - // Operator precedence val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21 ---- ====== @@ -297,7 +501,7 @@ listing shows both ways to use the assignment operator: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Inventor inventor = new Inventor(); EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); @@ -311,7 +515,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val inventor = Inventor() val context = SimpleEvaluationContext.forReadWriteDataBinding().build() @@ -325,3 +529,83 @@ Kotlin:: ====== +[[expressions-operators-overloaded]] +== Overloaded Operators + +By default, the mathematical operations defined in SpEL's `Operation` enum (`ADD`, +`SUBTRACT`, `DIVIDE`, `MULTIPLY`, `MODULUS`, and `POWER`) support simple types like +numbers. By providing an implementation of `OperatorOverloader`, the expression language +can support these operations on other types. + +For example, if we want to overload the `ADD` operator to allow two lists to be +concatenated using the `+` sign, we can implement a custom `OperatorOverloader` as +follows. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + pubic class ListConcatenation implements OperatorOverloader { + + @Override + public boolean overridesOperation(Operation operation, Object left, Object right) { + return (operation == Operation.ADD && + left instanceof List && right instanceof List); + } + + @Override + @SuppressWarnings("unchecked") + public Object operate(Operation operation, Object left, Object right) { + if (operation == Operation.ADD && + left instanceof List list1 && right instanceof List list2) { + + List result = new ArrayList(list1); + result.addAll(list2); + return result; + } + throw new UnsupportedOperationException( + "No overload for operation %s and operands [%s] and [%s]" + .formatted(operation, left, right)); + } + } +---- + +If we register `ListConcatenation` as the `OperatorOverloader` in a +`StandardEvaluationContext`, we can then evaluate expressions like `{1, 2, 3} + {4, 5}` +as demonstrated in the following example. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setOperatorOverloader(new ListConcatenation()); + + // evaluates to a new list: [1, 2, 3, 4, 5] + parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + StandardEvaluationContext context = StandardEvaluationContext() + context.setOperatorOverloader(ListConcatenation()) + + // evaluates to a new list: [1, 2, 3, 4, 5] + parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List::class.java) +---- +====== + +[NOTE] +==== +An `OperatorOverloader` does not change the default semantics for an operator. For +example, `2 + 2` in the above example still evaluates to `4`. +==== + +[CAUTION] +==== +Any expression that uses an overloaded operator cannot be compiled. See +xref:core/expressions/evaluation.adoc#expressions-compiler-limitations[Compiler Limitations] +for details. +==== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc index 3066b54dec7b..f00e2485c36a 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc @@ -1,31 +1,47 @@ [[expressions-properties-arrays]] = Properties, Arrays, Lists, Maps, and Indexers -Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were -populated with data listed in the xref:core/expressions/example-classes.adoc[Classes used in the examples] - section. To navigate "down" the object graph and get Tesla's year of birth and -Pupin's city of birth, we use the following expressions: +The Spring Expression Language provides support for navigating object graphs and indexing +into various structures. + +NOTE: Numerical index values are zero-based, such as when accessing the n^th^ element of +an array in Java. + +TIP: See the xref:core/expressions/language-ref/operator-safe-navigation.adoc[Safe Navigation Operator] +section for details on how to navigate object graphs and index into various structures +using the null-safe operator. + +[[expressions-property-navigation]] +== Property Navigation + +You can navigate property references within an object graph by using a period to indicate +a nested property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the +xref:core/expressions/example-classes.adoc[Classes used in the examples] section. To +navigate _down_ the object graph and get Tesla's year of birth and Pupin's city of birth, +we use the following expressions: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // evaluates to 1856 int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context); + // evaluates to "Smiljan" String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // evaluates to 1856 val year = parser.parseExpression("birthdate.year + 1900").getValue(context) as Int + // evaluates to "Smiljan" val city = parser.parseExpression("placeOfBirth.city").getValue(context) as String ---- ====== @@ -39,14 +55,26 @@ method invocations -- for example, `getPlaceOfBirth().getCity()` instead of `placeOfBirth.city`. ==== -The contents of arrays and lists are obtained by using square bracket notation, as the -following example shows: +[[expressions-indexing-arrays-and-collections]] +== Indexing into Arrays and Collections + +The n^th^ element of an array or collection (for example, a `Set` or `List`) can be +obtained by using square bracket notation, as the following example shows. + +[NOTE] +==== +If the indexed collection is a `java.util.List`, the n^th^ element will be accessed +directly via `list.get(n)`. + +For any other type of `Collection`, the n^th^ element will be accessed by iterating over +the collection using its `Iterator` and returning the n^th^ element encountered. +==== [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); @@ -63,7 +91,8 @@ Java:: String name = parser.parseExpression("members[0].name").getValue( context, ieee, String.class); - // List and Array navigation + // List and Array Indexing + // evaluates to "Wireless communication" String invention = parser.parseExpression("members[0].inventions[6]").getValue( context, ieee, String.class); @@ -71,7 +100,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() @@ -88,55 +117,248 @@ Kotlin:: val name = parser.parseExpression("members[0].name").getValue( context, ieee, String::class.java) - // List and Array navigation + // List and Array Indexing + // evaluates to "Wireless communication" val invention = parser.parseExpression("members[0].inventions[6]").getValue( context, ieee, String::class.java) ---- ====== -The contents of maps are obtained by specifying the literal key value within the -brackets. In the following example, because keys for the `officers` map are strings, we can specify -string literals: +[[expressions-indexing-strings]] +== Indexing into Strings + +The n^th^ character of a string can be obtained by specifying the index within square +brackets, as demonstrated in the following example. + +NOTE: The n^th^ character of a string will evaluate to a `java.lang.String`, not a +`java.lang.Character`. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "T" (8th letter of "Nikola Tesla") + String character = parser.parseExpression("members[0].name[7]") + .getValue(societyContext, String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "T" (8th letter of "Nikola Tesla") + val character = parser.parseExpression("members[0].name[7]") + .getValue(societyContext, String::class.java) +---- +====== + +[[expressions-indexing-maps]] +== Indexing into Maps + +The contents of maps are obtained by specifying the key value within square brackets. In +the following example, because keys for the `officers` map are strings, we can specify +string literals such as `'president'`: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - // Officer's Dictionary + // Officer's Map - Inventor pupin = parser.parseExpression("officers['president']").getValue( - societyContext, Inventor.class); + // evaluates to Inventor("Pupin") + Inventor pupin = parser.parseExpression("officers['president']") + .getValue(societyContext, Inventor.class); // evaluates to "Idvor" - String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue( - societyContext, String.class); + String city = parser.parseExpression("officers['president'].placeOfBirth.city") + .getValue(societyContext, String.class); + + String countryExpression = "officers['advisors'][0].placeOfBirth.country"; // setting values - parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue( - societyContext, "Croatia"); + parser.parseExpression(countryExpression) + .setValue(societyContext, "Croatia"); + + // evaluates to "Croatia" + String country = parser.parseExpression(countryExpression) + .getValue(societyContext, String.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - // Officer's Dictionary + // Officer's Map - val pupin = parser.parseExpression("officers['president']").getValue( - societyContext, Inventor::class.java) + // evaluates to Inventor("Pupin") + val pupin = parser.parseExpression("officers['president']") + .getValue(societyContext, Inventor::class.java) // evaluates to "Idvor" - val city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue( - societyContext, String::class.java) + val city = parser.parseExpression("officers['president'].placeOfBirth.city") + .getValue(societyContext, String::class.java) + + val countryExpression = "officers['advisors'][0].placeOfBirth.country" // setting values - parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue( - societyContext, "Croatia") + parser.parseExpression(countryExpression) + .setValue(societyContext, "Croatia") + + // evaluates to "Croatia" + val country = parser.parseExpression(countryExpression) + .getValue(societyContext, String::class.java) +---- +====== + +[[expressions-indexing-objects]] +== Indexing into Objects + +A property of an object can be obtained by specifying the name of the property within +square brackets. This is analogous to accessing the value of a map based on its key. The +following example demonstrates how to _index_ into an object to retrieve a specific +property. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // Create an inventor to use as the root context object. + Inventor tesla = new Inventor("Nikola Tesla"); + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression("#root['name']") + .getValue(context, tesla, String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Create an inventor to use as the root context object. + val tesla = Inventor("Nikola Tesla") + + // evaluates to "Nikola Tesla" + val name = parser.parseExpression("#root['name']") + .getValue(context, tesla, String::class.java) +---- +====== + +[[expressions-indexing-custom]] +== Indexing into Custom Structures + +Since Spring Framework 6.2, the Spring Expression Language supports indexing into custom +structures by allowing developers to implement and register an `IndexAccessor` with the +`EvaluationContext`. If you would like to support +xref:core/expressions/evaluation.adoc#expressions-spel-compilation[compilation] of +expressions that rely on a custom index accessor, that index accessor must implement the +`CompilableIndexAccessor` SPI. + +To support common use cases, Spring provides a built-in `ReflectiveIndexAccessor` which +is a flexible `IndexAccessor` that uses reflection to read from and optionally write to +an indexed structure of a target object. The indexed structure can be accessed through a +`public` read-method (when being read) or a `public` write-method (when being written). +The relationship between the read-method and write-method is based on a convention that +is applicable for typical implementations of indexed structures. + +NOTE: `ReflectiveIndexAccessor` also implements `CompilableIndexAccessor` in order to +support xref:core/expressions/evaluation.adoc#expressions-spel-compilation[compilation] +to bytecode for read access. Note, however, that the configured read-method must be +invokable via a `public` class or `public` interface for compilation to succeed. + +The following code listings define a `Color` enum and `FruitMap` type that behaves like a +map but does not implement the `java.util.Map` interface. Thus, if you want to index into +a `FruitMap` within a SpEL expression, you will need to register an `IndexAccessor`. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + package example; + + public enum Color { + RED, ORANGE, YELLOW + } +---- + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class FruitMap { + + private final Map map = new HashMap<>(); + + public FruitMap() { + this.map.put(Color.RED, "cherry"); + this.map.put(Color.ORANGE, "orange"); + this.map.put(Color.YELLOW, "banana"); + } + + public String getFruit(Color color) { + return this.map.get(color); + } + + public void setFruit(Color color, String fruit) { + this.map.put(color, fruit); + } + } ---- + +A read-only `IndexAccessor` for `FruitMap` can be created via `new +ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit")`. With that accessor +registered and a `FruitMap` registered as a variable named `#fruitMap`, the SpEL +expression `#fruitMap[T(example.Color).RED]` will evaluate to `"cherry"`. + +A read-write `IndexAccessor` for `FruitMap` can be created via `new +ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit", "setFruit")`. With that +accessor registered and a `FruitMap` registered as a variable named `#fruitMap`, the SpEL +expression `#fruitMap[T(example.Color).RED] = 'strawberry'` can be used to change the +fruit mapping for the color red from `"cherry"` to `"strawberry"`. + +The following example demonstrates how to register a `ReflectiveIndexAccessor` to index +into a `FruitMap` and then index into the `FruitMap` within a SpEL expression. + +[tabs] ====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // Create a ReflectiveIndexAccessor for FruitMap + IndexAccessor fruitMapAccessor = new ReflectiveIndexAccessor( + FruitMap.class, Color.class, "getFruit", "setFruit"); + // Register the IndexAccessor for FruitMap + context.addIndexAccessor(fruitMapAccessor); + // Register the fruitMap variable + context.setVariable("fruitMap", new FruitMap()); + + // evaluates to "cherry" + String fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]") + .getValue(context, String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Create a ReflectiveIndexAccessor for FruitMap + val fruitMapAccessor = ReflectiveIndexAccessor( + FruitMap::class.java, Color::class.java, "getFruit", "setFruit") + + // Register the IndexAccessor for FruitMap + context.addIndexAccessor(fruitMapAccessor) + + // Register the fruitMap variable + context.setVariable("fruitMap", FruitMap()) + + // evaluates to "cherry" + val fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]") + .getValue(context, String::class.java) +---- +====== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/templating.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/templating.adoc index 2b4a02d24f41..6961adee6ba7 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/templating.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/templating.adoc @@ -1,16 +1,16 @@ [[expressions-templating]] -= Expression templating += Expression Templating Expression templates allow mixing literal text with one or more evaluation blocks. Each evaluation block is delimited with prefix and suffix characters that you can -define. A common choice is to use `#{ }` as the delimiters, as the following example +define. A common choice is to use `+#{ }+` as the delimiters, as the following example shows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String randomPhrase = parser.parseExpression( "random number is #{T(java.lang.Math).random()}", @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val randomPhrase = parser.parseExpression( "random number is #{T(java.lang.Math).random()}", @@ -32,53 +32,13 @@ Kotlin:: ====== The string is evaluated by concatenating the literal text `'random number is '` with the -result of evaluating the expression inside the `#{ }` delimiter (in this case, the result -of calling that `random()` method). The second argument to the `parseExpression()` method -is of the type `ParserContext`. The `ParserContext` interface is used to influence how -the expression is parsed in order to support the expression templating functionality. -The definition of `TemplateParserContext` follows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class TemplateParserContext implements ParserContext { - - public String getExpressionPrefix() { - return "#{"; - } - - public String getExpressionSuffix() { - return "}"; - } - - public boolean isTemplate() { - return true; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class TemplateParserContext : ParserContext { - - override fun getExpressionPrefix(): String { - return "#{" - } - - override fun getExpressionSuffix(): String { - return "}" - } - - override fun isTemplate(): Boolean { - return true - } - } ----- -====== +result of evaluating the expression inside the `+#{ }+` delimiters (in this case, the +result of calling that `random()` method). The second argument to the `parseExpression()` +method is of the type `ParserContext`. The `ParserContext` interface is used to influence +how the expression is parsed in order to support the expression templating functionality. +The `TemplateParserContext` used in the previous example resides in the +`org.springframework.expression.common` package and is an implementation of the +`ParserContext` which by default configures the prefix and suffix to `#{` and `}`, +respectively. diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/types.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/types.adoc index 3d501f0de670..0068ebab819b 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/types.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/types.adoc @@ -13,7 +13,7 @@ following example shows how to use the `T` operator: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); @@ -26,7 +26,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class::class.java) diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/varargs.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/varargs.adoc new file mode 100644 index 000000000000..8b7240a13e71 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/varargs.adoc @@ -0,0 +1,151 @@ +[[expressions-varargs]] += Varargs Invocations + +The Spring Expression Language supports +https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html[varargs] +invocations for xref:core/expressions/language-ref/constructors.adoc[constructors], +xref:core/expressions/language-ref/methods.adoc[methods], and user-defined +xref:core/expressions/language-ref/functions.adoc[functions]. + +The following example shows how to invoke the `java.lang.String#formatted(Object...)` +_varargs_ method within an expression by supplying the variable argument list as separate +arguments (`'blue', 1`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "blue is color #1" + String expression = "'%s is color #%d'.formatted('blue', 1)"; + String message = parser.parseExpression(expression).getValue(String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "blue is color #1" + val expression = "'%s is color #%d'.formatted('blue', 1)" + val message = parser.parseExpression(expression).getValue(String::class.java) +---- +====== + +A variable argument list can also be supplied as an array, as demonstrated in the +following example (`new Object[] {'blue', 1}`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "blue is color #1" + String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})"; + String message = parser.parseExpression(expression).getValue(String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "blue is color #1" + val expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})" + val message = parser.parseExpression(expression).getValue(String::class.java) +---- +====== + +As an alternative, a variable argument list can be supplied as a `java.util.List` – for +example, as an xref:core/expressions/language-ref/inline-lists.adoc[inline list] +(`{'blue', 1}`). The following example shows how to do that. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "blue is color #1" + String expression = "'%s is color #%d'.formatted({'blue', 1})"; + String message = parser.parseExpression(expression).getValue(String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "blue is color #1" + val expression = "'%s is color #%d'.formatted({'blue', 1})" + val message = parser.parseExpression(expression).getValue(String::class.java) +---- +====== + +[[expressions-varargs-type-conversion]] +== Varargs Type Conversion + +In contrast to the standard support for varargs invocations in Java, +xref:core/expressions/evaluation.adoc#expressions-type-conversion[type conversion] may be +applied to the individual arguments when invoking varargs constructors, methods, or +functions in SpEL. + +For example, if we have registered a custom +xref:core/expressions/language-ref/functions.adoc[function] in the `EvaluationContext` +under the name `#reverseStrings` for a method with the signature +`String reverseStrings(String... strings)`, we can invoke that function within a SpEL +expression with any argument that can be converted to a `String`, as demonstrated in the +following example. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "3.0, 2.0, 1, SpEL" + String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)"; + String message = parser.parseExpression(expression) + .getValue(evaluationContext, String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "3.0, 2.0, 1, SpEL" + val expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)" + val message = parser.parseExpression(expression) + .getValue(evaluationContext, String::class.java) +---- +====== + +Similarly, any array whose component type is a subtype of the required varargs type can +be supplied as the variable argument list for a varargs invocation. For example, a +`String[]` array can be supplied to a varargs invocation that accepts an `Object...` +argument list. + +The following listing demonstrates that we can supply a `String[]` array to the +`java.lang.String#formatted(Object...)` _varargs_ method. It also highlights that `1` +will be automatically converted to `"1"`. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "blue is color #1" + String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})"; + String message = parser.parseExpression(expression).getValue(String.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // evaluates to "blue is color #1" + val expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})" + val message = parser.parseExpression(expression).getValue(String::class.java) +---- +====== + diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/variables.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/variables.adoc index 6f7ac6e971e9..70da818247c5 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/variables.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/variables.adoc @@ -1,27 +1,48 @@ [[expressions-ref-variables]] = Variables -You can reference variables in the expression by using the `#variableName` syntax. Variables -are set by using the `setVariable` method on `EvaluationContext` implementations. +You can reference variables in an expression by using the `#variableName` syntax. Variables +are set by using the `setVariable()` method in `EvaluationContext` implementations. [NOTE] ==== -Valid variable names must be composed of one or more of the following supported +Variable names must be begin with a letter (as defined below), an underscore, or a dollar +sign. + +Variable names must be composed of one or more of the following supported types of characters. -* letters: `A` to `Z` and `a` to `z` -* digits: `0` to `9` +* letter: any character for which `java.lang.Character.isLetter(char)` returns `true` + - This includes letters such as `A` to `Z`, `a` to `z`, `ü`, `ñ`, and `é` as well as + letters from other character sets such as Chinese, Japanese, Cyrillic, etc. +* digit: `0` to `9` * underscore: `_` * dollar sign: `$` ==== +[TIP] +==== +When setting a variable or root context object in the `EvaluationContext`, it is advised +that the type of the variable or root context object be `public`. + +Otherwise, certain types of SpEL expressions involving a variable or root context object +with a non-public type may fail to evaluate or compile. +==== + +[WARNING] +==== +Since variables share a common namespace with +xref:core/expressions/language-ref/functions.adoc[functions] in the evaluation context, +care must be taken to ensure that variable names and functions names do not overlap. +==== + The following example shows how to use variables. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); @@ -29,12 +50,12 @@ Java:: context.setVariable("newName", "Mike Tesla"); parser.parseExpression("name = #newName").getValue(context, tesla); - System.out.println(tesla.getName()) // "Mike Tesla" + System.out.println(tesla.getName()); // "Mike Tesla" ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val tesla = Inventor("Nikola Tesla", "Serbian") @@ -53,49 +74,106 @@ Kotlin:: The `#this` variable is always defined and refers to the current evaluation object (against which unqualified references are resolved). The `#root` variable is always defined and refers to the root context object. Although `#this` may vary as components of -an expression are evaluated, `#root` always refers to the root. The following examples -show how to use the `#this` and `#root` variables: +an expression are evaluated, `#root` always refers to the root. + +The following example shows how to use the `#this` variable in conjunction with +xref:core/expressions/language-ref/collection-selection.adoc[collection selection]. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - // create an array of integers - List primes = new ArrayList<>(); - primes.addAll(Arrays.asList(2,3,5,7,11,13,17)); + // Create a list of prime integers. + List primes = List.of(2, 3, 5, 7, 11, 13, 17); - // create parser and set variable 'primes' as the array of integers + // Create parser and set variable 'primes' as the list of integers. ExpressionParser parser = new SpelExpressionParser(); - EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess(); + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); context.setVariable("primes", primes); - // all prime numbers > 10 from the list (using selection ?{...}) - // evaluates to [11, 13, 17] - List primesGreaterThanTen = (List) parser.parseExpression( - "#primes.?[#this>10]").getValue(context); + // Select all prime numbers > 10 from the list (using selection ?{...}). + String expression = "#primes.?[#this > 10]"; + + // Evaluates to a list containing [11, 13, 17]. + List primesGreaterThanTen = + parser.parseExpression(expression).getValue(context, List.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - // create an array of integers - val primes = ArrayList() - primes.addAll(listOf(2, 3, 5, 7, 11, 13, 17)) + // Create a list of prime integers. + val primes = listOf(2, 3, 5, 7, 11, 13, 17) - // create parser and set variable 'primes' as the array of integers + // Create parser and set variable 'primes' as the list of integers. val parser = SpelExpressionParser() - val context = SimpleEvaluationContext.forReadOnlyDataAccess() + val context = SimpleEvaluationContext.forReadWriteDataBinding().build() context.setVariable("primes", primes) - // all prime numbers > 10 from the list (using selection ?{...}) - // evaluates to [11, 13, 17] - val primesGreaterThanTen = parser.parseExpression( - "#primes.?[#this>10]").getValue(context) as List + // Select all prime numbers > 10 from the list (using selection ?{...}). + val expression = "#primes.?[#this > 10]" + + // Evaluates to a list containing [11, 13, 17]. + val primesGreaterThanTen = parser.parseExpression(expression) + .getValue(context) as List ---- ====== +The following example shows how to use the `#this` and `#root` variables together in +conjunction with +xref:core/expressions/language-ref/collection-projection.adoc[collection projection]. +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // Create parser and evaluation context. + ExpressionParser parser = new SpelExpressionParser(); + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + + // Create an inventor to use as the root context object. + Inventor tesla = new Inventor("Nikola Tesla"); + tesla.setInventions("Telephone repeater", "Tesla coil transformer"); + + // Iterate over all inventions of the Inventor referenced as the #root + // object, and generate a list of strings whose contents take the form + // " invented the ." (using projection !{...}). + String expression = "#root.inventions.![#root.name + ' invented the ' + #this + '.']"; + + // Evaluates to a list containing: + // "Nikola Tesla invented the Telephone repeater." + // "Nikola Tesla invented the Tesla coil transformer." + List results = parser.parseExpression(expression) + .getValue(context, tesla, List.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Create parser and evaluation context. + val parser = SpelExpressionParser() + val context = SimpleEvaluationContext.forReadWriteDataBinding().build() + + // Create an inventor to use as the root context object. + val tesla = Inventor("Nikola Tesla") + tesla.setInventions("Telephone repeater", "Tesla coil transformer") + + // Iterate over all inventions of the Inventor referenced as the #root + // object, and generate a list of strings whose contents take the form + // " invented the ." (using projection !{...}). + val expression = "#root.inventions.![#root.name + ' invented the ' + #this + '.']" + + // Evaluates to a list containing: + // "Nikola Tesla invented the Telephone repeater." + // "Nikola Tesla invented the Tesla coil transformer." + val results = parser.parseExpression(expression) + .getValue(context, tesla, List::class.java) +---- +====== diff --git a/framework-docs/modules/ROOT/pages/core/null-safety.adoc b/framework-docs/modules/ROOT/pages/core/null-safety.adoc index e43ad7d0f41b..5264259da139 100644 --- a/framework-docs/modules/ROOT/pages/core/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/core/null-safety.adoc @@ -1,57 +1,184 @@ [[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 null-safety with its type system, the Spring Framework codebase is annotated with +https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullness of APIs, fields and related type +usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly recommended in order to get +familiar with those annotations and semantics. -* {api-spring-framework}/lang/Nullable.html[`@Nullable`]: Annotation to indicate that a -specific parameter, return value, or field can be `null`. -* {api-spring-framework}/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). -* {api-spring-framework}/lang/NonNullApi.html[`@NonNullApi`]: Annotation at the package level -that declares non-null as the default semantics for parameters and return values. -* {api-spring-framework}/lang/NonNullFields.html[`@NonNullFields`]: Annotation at the package -level that declares non-null as the default semantics for fields. +The primary goal of this explicit null-safety arrangement is to prevent `NullPointerException` to be thrown at runtime via +build time checks and to turn explicit nullness into a way to express the possible absence of value. It is useful in +both Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting null-safety +annotations such as IntelliJ IDEA or Eclipse) and Kotlin where JSpecify annotations are automatically translated to +{kotlin-docs}/null-safety.html[Kotlin's null safety]. -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. +The {spring-framework-api}/core/Nullness.html[`Nullness` Spring API] can be used at runtime to detect the nullness of a +type usage, a field, a method return type or a parameter. It provides full support for JSpecify annotations, +Kotlin null safety, Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the +package). -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. +[[null-safety-libraries]] +== Annotating libraries with JSpecify annotations +As of Spring Framework 7, the Spring Framework codebase leverages JSpecify annotations to expose null-safe APIs and +to check the consistency of those null-safety declarations with https://github.com/uber/NullAway[NullAway] as part of +its build. It is recommended for each library depending on Spring Framework (Spring portfolio projects), as +well as other libraries related to the Spring ecosystem (Reactor, Micrometer and Spring community projects), to do the +same. +[[null-safety-applications]] +== Leveraging JSpecify annotations in Spring applications +Developing applications with IDEs supporting null-safety annotations, such as IntelliJ IDEA or Eclipse, will provide +warnings in Java and errors in Kotlin when the null-safety contracts are not honored, allowing Spring application +developers to refine their null handling to prevent `NullPointerException` to be thrown at runtime. -[[use-cases]] -== Use cases +Optionally, Spring application developers can annotate their codebase and use https://github.com/uber/NullAway[NullAway] +to enforce null-safety during build time at application level. -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. +[[null-safety-guidelines]] +== Guidelines -They are also used to make Spring APIs null-safe in Kotlin projects, since Kotlin natively -supports https://kotlinlang.org/docs/null-safety.html[null-safety]. More details -are available in the xref:languages/kotlin/null-safety.adoc[Kotlin support documentation]. +The purpose of this section is to share some guidelines proposed for specifying explicitly the nullness of Spring-related +libraries or applications. + + +[[null-safety-guidelines-jpecify]] +=== JSpecify + +The key points to understand is that by default, the nullness of types is unknown in Java, and that non-null type +usages are by far more frequent than nullable ones. In order to keep codebases readable, we typically want to define +that by default, type usages are non-null unless marked as nullable for a specific scope. This is exactly the purpose of +https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`] that is typically set with Spring +at package level via a `package-info.java` file, for example: +[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 usages are 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. + +For example, for a field: + +[source,java,subs="verbatim,quotes"] +---- +private @Nullable String fileEncoding; +---- + +Or for method parameters and return value: + +[source,java,subs="verbatim,quotes"] +---- +public static @Nullable String buildMessage(@Nullable String message, + @Nullable Throwable cause) { + // ... +} +---- + +When overriding a method, nullness annotations are not inherited from the superclass method. That means those +nullness annotations should be repeated if you just want to override the implementation and keep the same API +nullness. + +With arrays and varargs, you need to be able to differentiate the nullness of the elements from the nullness of +the array itself. Pay attention to the syntax +https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.7.4[defined by the Java specification] which may be +initially surprising: + +- `@Nullable Object[] array` means individual elements can be null but the array itself can't. +- `Object @Nullable [] array` means individual elements can't be null but the array itself can. +- `@Nullable Object @Nullable [] array` means both individual elements and the array can be null. + +The Java specifications also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify +`@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 nullness checks only for packages annotated with `@NullMarked`. + - `NullAway:CustomContractAnnotations=org.springframework.lang.Contract` which makes NullAway aware of the +{spring-framework-api}/lang/Contract.html[@Contract] annotation in the `org.springframework.lang` package which +can be used to express complementary semantics to avoid non-relevant null-safety warnings in your codebase. +A good example of `@Contract` benefits is +{spring-framework-api}/util/Assert.html#notNull(java.lang.Object,java.lang.String)[`Assert#notnull`] which is annotated +with `@Contract("null, _ -> fail")`. With the configuration above, NullAway will understand that after a successful +invocation, the value passed as a parameter is not null. + +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 above. +==== Warnings suppression + +There are a few valid use cases where NullAway will wrongly detect nullness 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 nullness problem will never happen. + - `@SuppressWarnings("NullAway") // Lambda` can be used when NullAway does not take into account assertions performed +outside of a lambda for the code path within the lambda. +- `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known returning +non-null values even if that can't be expressed by the API. +- `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are done with keys known +to be present and non-null related values inserted previously. +- `@SuppressWarnings("NullAway") // Overridden method does not define nullness` can be used when the super class does +not define nullness (typically when the super class is coming from a dependency). + + +[[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 have been +introduced in Spring Framework 5 when JSpecify did not exist and the best option was to leverage JSR 305 (a dormant +but widespread JSR) meta-annotations. They are deprecated as of Spring Framework 7 in favor of +https://jspecify.dev/docs/start-here/[JSpecify] annotations, which provide significant enhancements such as properly +defined specifications, a canonical dependency with no split-package issue, better tooling, better Kotlin integration +and the capability to specify the nullness more precisely for more use cases. + +A key difference is that Spring null-safety annotations, following JSR 305 semantics, apply to fields, +parameters and return values while JSpecify annotations apply to type usages. This subtle difference +is in practice pretty significant, as it allows for example to differentiate the nullness of elements from the +nullness of arrays/varargs as well as defining the nullness of generic types. + +That means array and varargs null-safety declarations have to be updated to keep the same semantic. For example +`@Nullable Object[] array` with Spring annotations needs to be changed to `Object @Nullable [] array` with JSpecify +annotations. Same for varargs. + +It is also recommended to move field and return value annotations closer to the type, for example: + + - For fields, instead of `@Nullable private String field` with Spring annotations, use `private @Nullable String field` +with JSpecify annotations. +- For return values, instead of `@Nullable public String method()` with Spring annotations, use +`public @Nullable String method()` with JSpecify annotations. + +Also, with JSpecify, you don't need to specify `@NonNull` when overriding a type usage annotated with `@Nullable` in the +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. -[[jsr-305-meta-annotations]] -== JSR-305 meta-annotations - -Spring annotations are meta-annotated with https://jcp.org/en/jsr/detail?id=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/resources.adoc b/framework-docs/modules/ROOT/pages/core/resources.adoc index 3bfc74aeb33f..dee9df4f1abe 100644 --- a/framework-docs/modules/ROOT/pages/core/resources.adoc +++ b/framework-docs/modules/ROOT/pages/core/resources.adoc @@ -37,7 +37,7 @@ such as a method to check for the existence of the resource being pointed to. Spring's `Resource` interface located in the `org.springframework.core.io.` package is meant to be a more capable interface for abstracting access to low-level resources. The following listing provides an overview of the `Resource` interface. See the -{api-spring-framework}/core/io/Resource.html[`Resource`] javadoc for further details. +{spring-framework-api}/core/io/Resource.html[`Resource`] javadoc for further details. [source,java,indent=0,subs="verbatim,quotes"] @@ -104,7 +104,7 @@ resource (if the underlying implementation is compatible and supports that functionality). Some implementations of the `Resource` interface also implement the extended -{api-spring-framework}/core/io/WritableResource.html[`WritableResource`] interface +{spring-framework-api}/core/io/WritableResource.html[`WritableResource`] interface for a resource that supports writing to it. Spring itself uses the `Resource` abstraction extensively, as an argument type in @@ -143,7 +143,7 @@ Spring includes several built-in `Resource` implementations: For a complete list of `Resource` implementations available in Spring, consult the "All Known Implementing Classes" section of the -{api-spring-framework}/core/io/Resource.html[`Resource`] javadoc. +{spring-framework-api}/core/io/Resource.html[`Resource`] javadoc. @@ -280,14 +280,14 @@ snippet of code was run against a `ClassPathXmlApplicationContext` instance: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Resource template = ctx.getResource("some/resource/path/myTemplate.txt"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val template = ctx.getResource("some/resource/path/myTemplate.txt") ---- @@ -309,14 +309,14 @@ example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt") ---- @@ -329,14 +329,14 @@ Similarly, you can force a `UrlResource` to be used by specifying any of the sta ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val template = ctx.getResource("file:///some/resource/path/myTemplate.txt") ---- @@ -346,14 +346,14 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt") ---- @@ -505,7 +505,7 @@ property of type `Resource`. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- package example; @@ -523,7 +523,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyBean(var template: Resource) ---- @@ -571,7 +571,7 @@ The following example demonstrates how to achieve this. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class MyBean { @@ -588,7 +588,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class MyBean(@Value("\${template.path}") private val template: Resource) @@ -606,7 +606,7 @@ can be injected into the `MyBean` constructor. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class MyBean { @@ -623,7 +623,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class MyBean(@Value("\${templates.path}") private val templates: Resource[]) @@ -657,14 +657,14 @@ specific application context. For example, consider the following example, which ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = ClassPathXmlApplicationContext("conf/appContext.xml") ---- @@ -677,7 +677,7 @@ used. However, consider the following example, which creates a `FileSystemXmlApp ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml"); @@ -685,7 +685,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = FileSystemXmlApplicationContext("conf/appContext.xml") ---- @@ -702,7 +702,7 @@ definitions. Consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml"); @@ -710,7 +710,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml") ---- @@ -749,7 +749,7 @@ classpath) can be instantiated: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext ctx = new ClassPathXmlApplicationContext( new String[] {"services.xml", "repositories.xml"}, MessengerService.class); @@ -757,13 +757,13 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "repositories.xml"), MessengerService::class.java) ---- ====== -See the {api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`] +See the {spring-framework-api}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`] javadoc for details on the various constructors. @@ -843,7 +843,7 @@ special `classpath*:` prefix, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml"); @@ -851,7 +851,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = ClassPathXmlApplicationContext("classpath*:conf/appContext.xml") ---- @@ -903,10 +903,10 @@ entries in the classpath. When you build JARs with Ant, do not activate the `fil switch of the JAR task. Also, classpath directories may not get exposed based on security policies in some environments -- for example, stand-alone applications on JDK 1.7.0_45 and higher (which requires 'Trusted-Library' to be set up in your manifests. See -https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources). +{stackoverflow-questions}/19394570/java-jre-7u45-breaks-classloader-getresources). -On JDK 9's module path (Jigsaw), Spring's classpath scanning generally works as expected. -Putting resources into a dedicated directory is highly recommendable here as well, +On the module path (Java Module System), Spring's classpath scanning generally works as +expected. Putting resources into a dedicated directory is highly recommendable here as well, avoiding the aforementioned portability problems with searching the jar file root level. ==== @@ -955,7 +955,7 @@ In practice, this means the following examples are equivalent: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/context.xml"); @@ -963,7 +963,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = FileSystemXmlApplicationContext("conf/context.xml") ---- @@ -973,7 +973,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext ctx = new FileSystemXmlApplicationContext("/conf/context.xml"); @@ -981,7 +981,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx = FileSystemXmlApplicationContext("/conf/context.xml") ---- @@ -994,7 +994,7 @@ case is relative and the other absolute): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- FileSystemXmlApplicationContext ctx = ...; ctx.getResource("some/resource/path/myTemplate.txt"); @@ -1002,7 +1002,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx: FileSystemXmlApplicationContext = ... ctx.getResource("some/resource/path/myTemplate.txt") @@ -1013,7 +1013,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- FileSystemXmlApplicationContext ctx = ...; ctx.getResource("/some/resource/path/myTemplate.txt"); @@ -1021,7 +1021,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val ctx: FileSystemXmlApplicationContext = ... ctx.getResource("/some/resource/path/myTemplate.txt") @@ -1037,7 +1037,7 @@ show how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // actual context type doesn't matter, the Resource will always be UrlResource ctx.getResource("file:///some/resource/path/myTemplate.txt"); @@ -1045,7 +1045,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // actual context type doesn't matter, the Resource will always be UrlResource ctx.getResource("file:///some/resource/path/myTemplate.txt") @@ -1056,7 +1056,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // force this FileSystemXmlApplicationContext to load its definition via a UrlResource ApplicationContext ctx = @@ -1065,7 +1065,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // force this FileSystemXmlApplicationContext to load its definition via a UrlResource val ctx = FileSystemXmlApplicationContext("file:///conf/context.xml") 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 768af0c34345..000000000000 --- a/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc +++ /dev/null @@ -1,47 +0,0 @@ -[[spring-jcl]] -= Logging - -Since Spring Framework 5.0, 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 -https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-logging[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",role="primary"] ----- -public class MyBean { - private final Log log = LogFactory.getLog(getClass()); - // ... -} ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- -class MyBean { - private val log = LogFactory.getLog(javaClass) - // ... -} ----- -====== diff --git a/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc b/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc index bc69769349c3..d522a57c990e 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc @@ -27,15 +27,20 @@ The target class should have a single public constructor or a single non-public with arguments. If there are multiple constructors, then a default constructor if present is used. -By default, constructor parameter names are used to look up argument values, but you can -configure a `NameResolver`. Spring MVC and WebFlux both rely to allow customizing the name -of the value to bind through an `@BindParam` annotation on constructor parameters. +By default, argument values are looked up via constructor parameter names. Spring MVC and +WebFlux support a custom name mapping through the `@BindParam` annotation on constructor +parameters or fields if present. If necessary, you can also configure a `NameResolver` on +`DataBinder` to customize the argument name to use. xref:beans-beans-conventions[Type conversion] is applied as needed to convert user input. If the constructor parameter is an object, it is constructed recursively in the same manner, but through a nested property path. That means constructor binding creates both the target object and any objects it contains. +Constructor binding supports `List`, `Map`, and array arguments either converted from +a single string, for example, comma-separated list, or based on indexed keys such as +`accounts[2].name` or `account[KEY].name`. + Binding and conversion errors are reflected in the `BindingResult` of the `DataBinder`. If the target is created successfully, then `target` is set to the created instance after the call to `construct`. @@ -51,7 +56,7 @@ A JavaBean is a class with a default no-argument constructor and that follows a naming convention where (for example) a property named `bingoMadness` would have a setter method `setBingoMadness(..)` and a getter method `getBingoMadness()`. For more information about JavaBeans and the specification, see -https://docs.oracle.com/javase/8/docs/api/java/beans/package-summary.html[javabeans]. +{java-api}/java.desktop/java/beans/package-summary.html[javabeans]. One quite important class in the beans package is the `BeanWrapper` interface and its corresponding implementation (`BeanWrapperImpl`). As quoted from the javadoc, the @@ -90,13 +95,12 @@ details. The below table shows some examples of these conventions: | Indicates the nested property `name` of the property `account` that corresponds to (for example) the `getAccount().setName()` or `getAccount().getName()` methods. -| `account[2]` +| `accounts[2]` | Indicates the _third_ element of the indexed property `account`. Indexed properties can be of type `array`, `list`, or other naturally ordered collection. -| `account[COMPANYNAME]` -| Indicates the value of the map entry indexed by the `COMPANYNAME` key of the `account` `Map` - property. +| `accounts[KEY]` +| Indicates the value of the map entry indexed by the `KEY` value. |=== (This next section is not vitally important to you if you do not plan to work with @@ -111,7 +115,7 @@ properties: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class Company { @@ -138,7 +142,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Company { var name: String? = null @@ -151,7 +155,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class Employee { @@ -179,7 +183,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Employee { var name: String? = null @@ -195,7 +199,7 @@ the properties of instantiated ``Company``s and ``Employee``s: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- BeanWrapper company = new BeanWrapperImpl(new Company()); // setting the company name.. @@ -215,7 +219,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val company = BeanWrapperImpl(Company()) // setting the company name.. @@ -249,7 +253,7 @@ behavior can be achieved by registering custom editors of type `java.beans.PropertyEditor`. Registering custom editors on a `BeanWrapper` or, alternatively, in a specific IoC container (as mentioned in the previous chapter), gives it the knowledge of how to convert properties to the desired type. For more about -`PropertyEditor`, see https://docs.oracle.com/javase/8/docs/api/java/beans/package-summary.html[the javadoc of the `java.beans` package from Oracle]. +`PropertyEditor`, see {java-api}/java.desktop/java/beans/package-summary.html[the javadoc of the `java.beans` package from Oracle]. A couple of examples where property editing is used in Spring: @@ -355,7 +359,7 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +{java-tutorial}/javabeans/advanced/customization.html[here]). The following example uses the `BeanInfo` mechanism to explicitly register one or more `PropertyEditor` instances with the properties of an associated class: @@ -375,7 +379,7 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SomethingBeanInfo extends SimpleBeanInfo { @@ -399,7 +403,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SomethingBeanInfo : SimpleBeanInfo() { @@ -463,7 +467,7 @@ another class called `DependsOnExoticType`, which needs `ExoticType` set as a pr ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package example; @@ -488,7 +492,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package example @@ -518,7 +522,7 @@ The `PropertyEditor` implementation could look similar to the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package example; @@ -535,7 +539,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package example @@ -589,7 +593,7 @@ The following example shows how to create your own `PropertyEditorRegistrar` imp ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo.editors.spring; @@ -607,7 +611,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package com.foo.editors.spring @@ -657,7 +661,7 @@ example uses a `PropertyEditorRegistrar` in the implementation of an `@InitBinde ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class RegisterUserController { @@ -679,7 +683,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class RegisterUserController( diff --git a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc index ef6bbb8630e8..f5d83d4ad705 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc @@ -2,7 +2,7 @@ = Java Bean Validation The Spring Framework provides support for the -https://beanvalidation.org/[Java Bean Validation] API. +{bean-validation-site}[Java Bean Validation] API. @@ -20,7 +20,7 @@ Consider the following example, which shows a simple `PersonForm` model with two ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class PersonForm { private String name; @@ -30,7 +30,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class PersonForm( private val name: String, @@ -45,7 +45,7 @@ Bean Validation lets you declare constraints as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class PersonForm { @@ -60,7 +60,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class PersonForm( @get:NotNull @get:Size(max=64) @@ -72,7 +72,7 @@ Kotlin:: ====== A Bean Validation validator then validates instances of this class based on the declared -constraints. See https://beanvalidation.org/[Bean Validation] for general information about +constraints. See {bean-validation-site}[Bean Validation] for general information about the API. See the https://hibernate.org/validator/[Hibernate Validator] documentation for specific constraints. To learn how to set up a bean validation provider as a Spring bean, keep reading. @@ -94,7 +94,7 @@ bean, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; @@ -110,7 +110,7 @@ Java:: XML:: + -[source,xml,indent=0,subs="verbatim,quotes",role="secondary"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -134,7 +134,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import jakarta.validation.Validator; @@ -148,7 +148,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import jakarta.validation.Validator; @@ -171,7 +171,7 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.validation.Validator; @@ -185,7 +185,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.validation.Validator @@ -196,7 +196,7 @@ Kotlin:: When used as `org.springframework.validation.Validator`, `LocalValidatorFactoryBean` invokes the underlying `jakarta.validation.Validator`, and then adapts -``ContraintViolation``s to ``FieldError``s, and registers them with the `Errors` object +``ConstraintViolation``s to ``FieldError``s, and registers them with the `Errors` object passed into the `validate` method. @@ -226,7 +226,7 @@ The following example shows a custom `@Constraint` declaration followed by an as ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @@ -237,7 +237,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) @@ -250,7 +250,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import jakarta.validation.ConstraintValidator; @@ -265,7 +265,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import jakarta.validation.ConstraintValidator @@ -287,37 +287,12 @@ As the preceding example shows, a `ConstraintValidator` implementation can have You can integrate the method validation feature of Bean Validation into a Spring context through a `MethodValidationPostProcessor` bean definition: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; - - @Configuration - public class AppConfig { - - @Bean - public MethodValidationPostProcessor validationPostProcessor() { - return new MethodValidationPostProcessor(); - } - } - ----- - -XML:: -+ -[source,xml,indent=0,subs="verbatim,quotes",role="secondary"] ----- - ----- -====== +include-code::./ApplicationConfiguration[tag=snippet,indent=0] To be eligible for Spring-driven method validation, target classes need to be annotated with Spring's `@Validated` annotation, which can optionally also declare the validation groups to use. See -{api-spring-framework}/validation/beanvalidation/MethodValidationPostProcessor.html[`MethodValidationPostProcessor`] +{spring-framework-api}/validation/beanvalidation/MethodValidationPostProcessor.html[`MethodValidationPostProcessor`] for setup details with the Hibernate Validator and Bean Validation providers. [TIP] @@ -341,40 +316,11 @@ xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] sections. === Method Validation Exceptions By default, `jakarta.validation.ConstraintViolationException` is raised with the set of -``ConstraintViolation``s returned by `jakarata.validation.Validator`. As an alternative, +``ConstraintViolation``s returned by `jakarta.validation.Validator`. As an alternative, you can have `MethodValidationException` raised instead with ``ConstraintViolation``s adapted to `MessageSourceResolvable` errors. To enable set the following flag: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; - - @Configuration - public class AppConfig { - - @Bean - public MethodValidationPostProcessor validationPostProcessor() { - MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); - processor.setAdaptConstraintViolations(true); - return processor; - } - } - ----- - -XML:: -+ -[source,xml,indent=0,subs="verbatim,quotes",role="secondary"] ----- - - - ----- -====== +include-code::./ApplicationConfiguration[tag=snippet,indent=0] `MethodValidationException` contains a list of ``ParameterValidationResult``s which group errors by method parameter, and each exposes a `MethodParameter`, the argument @@ -398,7 +344,7 @@ Given the following class declarations: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- record Person(@Size(min = 1, max = 10) String name) { } @@ -414,7 +360,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @JvmRecord internal data class Person(@Size(min = 1, max = 10) val name: String) @@ -429,38 +375,38 @@ Kotlin:: ---- ====== -A `ConstraintViolation` on `Person.name()` is adapted to a `FieldErrro` with the following: +A `ConstraintViolation` on `Person.name()` is adapted to a `FieldError` with the following: -- Error codes `"Size.student.name"`, `"Size.name"`, `"Size.java.lang.String"`, and `"Size"` +- Error codes `"Size.person.name"`, `"Size.name"`, `"Size.java.lang.String"`, and `"Size"` - Message arguments `"name"`, `10`, and `1` (the field name and the constraint attributes) - Default message "size must be between 1 and 10" To customize the default message, you can add properties to xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource] resource bundles using any of the above errors codes and message arguments. Note also that the -message argument `"name"` is itself a `MessagreSourceResolvable` with error codes -`"student.name"` and `"name"` and can customized too. For example: +message argument `"name"` is itself a `MessageSourceResolvable` with error codes +`"person.name"` and `"name"` and can be customized too. For example: Properties:: + -[source,properties,indent=0,subs="verbatim,quotes",role="secondary"] +[source,properties,indent=0,subs="verbatim,quotes"] ---- -Size.student.name=Please, provide a {0} that is between {2} and {1} characters long -student.name=username +Size.person.name=Please, provide a {0} that is between {2} and {1} characters long +person.name=username ---- 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: Properties:: + -[source,properties,indent=0,subs="verbatim,quotes",role="secondary"] +[source,properties,indent=0,subs="verbatim,quotes"] ---- Max.degrees=You cannot provide more than {1} {0} ---- @@ -472,7 +418,7 @@ Max.degrees=You cannot provide more than {1} {0} The default `LocalValidatorFactoryBean` configuration suffices for most cases. There are a number of configuration options for various Bean Validation constructs, from message interpolation to traversal resolution. See the -{api-spring-framework}/validation/beanvalidation/LocalValidatorFactoryBean.html[`LocalValidatorFactoryBean`] +{spring-framework-api}/validation/beanvalidation/LocalValidatorFactoryBean.html[`LocalValidatorFactoryBean`] javadoc for more information on these options. @@ -491,7 +437,7 @@ logic after binding to a target object: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Foo target = new Foo(); DataBinder binder = new DataBinder(target); @@ -509,7 +455,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val target = Foo() val binder = DataBinder(target) diff --git a/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc b/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc index 49deddde0c33..37c62169572f 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc @@ -19,8 +19,8 @@ of the field). This is done as a convenience to aid developers when targeting er More information on the `MessageCodesResolver` and the default strategy can be found in the javadoc of -{api-spring-framework}/validation/MessageCodesResolver.html[`MessageCodesResolver`] and -{api-spring-framework}/validation/DefaultMessageCodesResolver.html[`DefaultMessageCodesResolver`], +{spring-framework-api}/validation/MessageCodesResolver.html[`MessageCodesResolver`] and +{spring-framework-api}/validation/DefaultMessageCodesResolver.html[`DefaultMessageCodesResolver`], respectively. diff --git a/framework-docs/modules/ROOT/pages/core/validation/convert.adoc b/framework-docs/modules/ROOT/pages/core/validation/convert.adoc index 8f0f15486d5d..adf928eef7a1 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/convert.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/convert.adoc @@ -263,7 +263,7 @@ it like you would for any other bean. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Service public class MyService { @@ -282,7 +282,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Service class MyService(private val conversionService: ConversionService) { @@ -306,7 +306,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- DefaultConversionService cs = new DefaultConversionService(); @@ -318,7 +318,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val cs = DefaultConversionService() diff --git a/framework-docs/modules/ROOT/pages/core/validation/format-configuring-formatting-globaldatetimeformat.adoc b/framework-docs/modules/ROOT/pages/core/validation/format-configuring-formatting-globaldatetimeformat.adoc index 82fc655f1310..1b67d8467a1c 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/format-configuring-formatting-globaldatetimeformat.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/format-configuring-formatting-globaldatetimeformat.adoc @@ -11,106 +11,9 @@ formatters manually with the help of: * `org.springframework.format.datetime.standard.DateTimeFormatterRegistrar` * `org.springframework.format.datetime.DateFormatterRegistrar` -For example, the following Java configuration registers a global `yyyyMMdd` format: +For example, the following configuration registers a global `yyyyMMdd` format: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - public class AppConfig { - - @Bean - public FormattingConversionService conversionService() { - - // Use the DefaultFormattingConversionService but do not register defaults - DefaultFormattingConversionService conversionService = - new DefaultFormattingConversionService(false); - - // Ensure @NumberFormat is still supported - conversionService.addFormatterForFieldAnnotation( - new NumberFormatAnnotationFormatterFactory()); - - // Register JSR-310 date conversion with a specific global format - DateTimeFormatterRegistrar dateTimeRegistrar = new DateTimeFormatterRegistrar(); - dateTimeRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")); - dateTimeRegistrar.registerFormatters(conversionService); - - // Register date conversion with a specific global format - DateFormatterRegistrar dateRegistrar = new DateFormatterRegistrar(); - dateRegistrar.setFormatter(new DateFormatter("yyyyMMdd")); - dateRegistrar.registerFormatters(conversionService); - - return conversionService; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - class AppConfig { - - @Bean - fun conversionService(): FormattingConversionService { - // Use the DefaultFormattingConversionService but do not register defaults - return DefaultFormattingConversionService(false).apply { - - // Ensure @NumberFormat is still supported - addFormatterForFieldAnnotation(NumberFormatAnnotationFormatterFactory()) - - // Register JSR-310 date conversion with a specific global format - val dateTimeRegistrar = DateTimeFormatterRegistrar() - dateTimeRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")) - dateTimeRegistrar.registerFormatters(this) - - // Register date conversion with a specific global format - val dateRegistrar = DateFormatterRegistrar() - dateRegistrar.setFormatter(DateFormatter("yyyyMMdd")) - dateRegistrar.registerFormatters(this) - } - } - } ----- -====== - -If you prefer XML-based configuration, you can use a -`FormattingConversionServiceFactoryBean`. The following example shows how to do so: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - - - - - - - - - - ----- +include-code::./ApplicationConfiguration[tag=snippet,indent=0] Note there are extra considerations when configuring date and time formats in web applications. Please see diff --git a/framework-docs/modules/ROOT/pages/core/validation/format.adoc b/framework-docs/modules/ROOT/pages/core/validation/format.adoc index 920e2a44d970..1d8dea34d2d6 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/format.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/format.adoc @@ -74,7 +74,8 @@ The `format` subpackages provide several `Formatter` implementations as a conven The `number` package provides `NumberStyleFormatter`, `CurrencyStyleFormatter`, and `PercentStyleFormatter` to format `Number` objects that use a `java.text.NumberFormat`. The `datetime` package provides a `DateFormatter` to format `java.util.Date` objects with -a `java.text.DateFormat`. +a `java.text.DateFormat`, as well as a `DurationFormatter` to format `Duration` objects +in different styles defined in the `@DurationFormat.Style` enum (see <>). The following `DateFormatter` is an example `Formatter` implementation: @@ -82,7 +83,7 @@ The following `DateFormatter` is an example `Formatter` implementation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.springframework.format.datetime; @@ -118,7 +119,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- class DateFormatter(private val pattern: String) : Formatter { @@ -139,7 +140,7 @@ Kotlin:: ====== The Spring team welcomes community-driven `Formatter` contributions. See -https://github.com/spring-projects/spring-framework/issues[GitHub Issues] to contribute. +{spring-framework-issues}[GitHub Issues] to contribute. @@ -179,7 +180,7 @@ annotation to a formatter to let a number style or pattern be specified: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public final class NumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory { @@ -216,7 +217,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class NumberFormatAnnotationFormatterFactory : AnnotationFormatterFactory { @@ -255,7 +256,7 @@ example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyModel { @@ -266,7 +267,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyModel( @field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal @@ -280,17 +281,18 @@ Kotlin:: A portable format annotation API exists in the `org.springframework.format.annotation` package. You can use `@NumberFormat` to format `Number` fields such as `Double` and -`Long`, and `@DateTimeFormat` to format `java.util.Date`, `java.util.Calendar`, `Long` -(for millisecond timestamps) as well as JSR-310 `java.time`. +`Long`, `@DurationFormat` to format `Duration` fields in ISO-8601 and simplified styles, +and `@DateTimeFormat` to format fields such as `java.util.Date`, `java.util.Calendar`, +and `Long` (for millisecond timestamps) as well as JSR-310 `java.time` types. -The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO Date +The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO date (yyyy-MM-dd): [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyModel { @@ -301,7 +303,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyModel( @DateTimeFormat(iso=ISO.DATE) private val date: Date @@ -309,6 +311,29 @@ Kotlin:: ---- ====== +For further details, see the javadoc for +{spring-framework-api}/format/annotation/DateTimeFormat.html[`@DateTimeFormat`], +{spring-framework-api}/format/annotation/DurationFormat.html[`@DurationFormat`], and +{spring-framework-api}/format/annotation/NumberFormat.html[`@NumberFormat`]. + +[WARNING] +==== +Style-based formatting and parsing rely on locale-sensitive patterns which may change +depending on the Java runtime. Specifically, applications that rely on date, time, or +number parsing and formatting may encounter incompatible changes in behavior when running +on JDK 20 or higher. + +Using an ISO standardized format or a concrete pattern that you control allows for +reliable system-independent and locale-independent parsing and formatting of date, time, +and number values. + +For `@DateTimeFormat`, the use of fallback patterns can also help to address +compatibility issues. + +For further details, see the +https://github.com/spring-projects/spring-framework/wiki/Date-and-Time-Formatting-with-JDK-20-and-higher[Date and Time Formatting with JDK 20 and higher] +page in the Spring Framework wiki. +==== [[format-FormatterRegistry-SPI]] == The `FormatterRegistry` SPI @@ -316,7 +341,7 @@ Kotlin:: The `FormatterRegistry` is an SPI for registering formatters and converters. `FormattingConversionService` is an implementation of `FormatterRegistry` suitable for most environments. You can programmatically or declaratively configure this variant -as a Spring bean, e.g. by using `FormattingConversionServiceFactoryBean`. Because this +as a Spring bean, for example, by using `FormattingConversionServiceFactoryBean`. Because this implementation also implements `ConversionService`, you can directly configure it for use with Spring's `DataBinder` and the Spring Expression Language (SpEL). diff --git a/framework-docs/modules/ROOT/pages/core/validation/validator.adoc b/framework-docs/modules/ROOT/pages/core/validation/validator.adoc index 8e3b060b54f1..1d446814a10b 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/validator.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/validator.adoc @@ -11,7 +11,7 @@ Consider the following example of a small data object: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class Person { @@ -24,7 +24,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Person(val name: String, val age: Int) ---- @@ -45,7 +45,7 @@ example implements `Validator` for `Person` instances: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class PersonValidator implements Validator { @@ -70,7 +70,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class PersonValidator : Validator { @@ -96,7 +96,7 @@ Kotlin:: The `static` `rejectIfEmpty(..)` method on the `ValidationUtils` class is used to reject the `name` property if it is `null` or the empty string. Have a look at the -{api-spring-framework}/validation/ValidationUtils.html[`ValidationUtils`] javadoc +{spring-framework-api}/validation/ValidationUtils.html[`ValidationUtils`] javadoc to see what functionality it provides besides the example shown previously. While it is certainly possible to implement a single `Validator` class to validate each @@ -114,7 +114,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class CustomerValidator implements Validator { @@ -155,7 +155,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CustomerValidator(private val addressValidator: Validator) : Validator { @@ -193,14 +193,14 @@ Kotlin:: Validation errors are reported to the `Errors` object passed to the validator. In the case of Spring Web MVC, you can use the `` tag to inspect the error messages, but you can also inspect the `Errors` object yourself. More information about the -methods it offers can be found in the {api-spring-framework}/validation/Errors.html[javadoc]. +methods it offers can be found in the {spring-framework-api}/validation/Errors.html[javadoc]. Validators may also get locally invoked for the immediate validation of a given object, not involving a binding process. As of 6.1, this has been simplified through a new `Validator.validateObject(Object)` method which is available by default now, returning -a simple ´Errors` representation which can be inspected: typically calling `hasErrors()` +a simple `Errors` representation which can be inspected: typically calling `hasErrors()` or the new `failOnError` method for turning the error summary message into an exception -(e.g. `validator.validateObject(myObject).failOnError(IllegalArgumentException::new)`). +(for example, `validator.validateObject(myObject).failOnError(IllegalArgumentException::new)`). diff --git a/framework-docs/modules/ROOT/pages/data-access/dao.adoc b/framework-docs/modules/ROOT/pages/data-access/dao.adoc index 9ca9666f5415..f4c1accbb007 100644 --- a/framework-docs/modules/ROOT/pages/data-access/dao.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/dao.adoc @@ -56,7 +56,7 @@ how to use the `@Repository` annotation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Repository // <1> public class SomeMovieFinder implements MovieFinder { @@ -67,7 +67,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repository // <1> class SomeMovieFinder : MovieFinder { @@ -89,7 +89,7 @@ annotations. The following example works for a JPA repository: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Repository public class JpaMovieFinder implements MovieFinder { @@ -103,7 +103,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repository class JpaMovieFinder : MovieFinder { @@ -124,7 +124,7 @@ example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Repository public class HibernateMovieFinder implements MovieFinder { @@ -142,7 +142,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repository class HibernateMovieFinder(private val sessionFactory: SessionFactory) : MovieFinder { @@ -160,7 +160,7 @@ and other data access support classes (such as `SimpleJdbcCall` and others) by u ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Repository public class JdbcMovieFinder implements MovieFinder { @@ -178,7 +178,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repository class JdbcMovieFinder(dataSource: DataSource) : MovieFinder { diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc index 04533c9cb822..91786b3dbdee 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc @@ -21,7 +21,7 @@ and the entire list is used as the batch: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -53,7 +53,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -104,7 +104,7 @@ The following example shows a batch update using named parameters: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -126,7 +126,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -155,7 +155,7 @@ JDBC `?` placeholders: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -183,7 +183,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -211,19 +211,28 @@ the JDBC driver. If the count is not available, the JDBC driver returns a value ==== In such a scenario, with automatic setting of values on an underlying `PreparedStatement`, the corresponding JDBC type for each value needs to be derived from the given Java type. -While this usually works well, there is a potential for issues (for example, with Map-contained -`null` values). Spring, by default, calls `ParameterMetaData.getParameterType` in such a -case, which can be expensive with your JDBC driver. You should use a recent driver +While this usually works well, there is a potential for issues (for example, with +Map-contained `null` values). Spring, by default, calls `ParameterMetaData.getParameterType` +in such a case, which can be expensive with your JDBC driver. You should use a recent driver version and consider setting the `spring.jdbc.getParameterType.ignore` property to `true` (as a JVM system property or via the -xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism) if you encounter -a performance issue (as reported on Oracle 12c, JBoss, and PostgreSQL). - -Alternatively, you might consider specifying the corresponding JDBC types explicitly, -either through a `BatchPreparedStatementSetter` (as shown earlier), through an explicit type -array given to a `List` based call, through `registerSqlType` calls on a -custom `MapSqlParameterSource` instance, or through a `BeanPropertySqlParameterSource` -that derives the SQL type from the Java-declared property type even for a null value. +xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism) +if you encounter a specific performance issue for your application. + +As of 6.1.2, Spring bypasses the default `getParameterType` resolution on PostgreSQL and +MS SQL Server. This is a common optimization to avoid further roundtrips to the DBMS just +for parameter type resolution which is known to make a very significant difference on +PostgreSQL and MS SQL Server specifically, in particular for batch operations. If you +happen to see a side effect, for example, when setting a byte array to null without specific type +indication, you may explicitly set the `spring.jdbc.getParameterType.ignore=false` flag +as a system property (see above) to restore full `getParameterType` resolution. + +Alternatively, you could consider specifying the corresponding JDBC types explicitly, +either through a `BatchPreparedStatementSetter` (as shown earlier), through an explicit +type array given to a `List` based call, through `registerSqlType` calls on a +custom `MapSqlParameterSource` instance, through a `BeanPropertySqlParameterSource` +that derives the SQL type from the Java-declared property type even for a null value, or +through providing individual `SqlParameterValue` instances instead of plain null values. ==== @@ -245,7 +254,7 @@ The following example shows a batch update that uses a batch size of 100: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -274,7 +283,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc index 7ebb4a44a68f..d862ef883869 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc @@ -46,47 +46,9 @@ To configure a `DriverManagerDataSource`: for the correct value.) . Provide a username and a password to connect to the database. -The following example shows how to configure a `DriverManagerDataSource` in Java: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - DriverManagerDataSource dataSource = new DriverManagerDataSource(); - dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); - dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); - dataSource.setUsername("sa"); - dataSource.setPassword(""); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val dataSource = DriverManagerDataSource().apply { - setDriverClassName("org.hsqldb.jdbcDriver") - url = "jdbc:hsqldb:hsql://localhost:" - username = "sa" - password = "" - } ----- -====== - -The following example shows the corresponding XML configuration: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- +The following example shows how to configure a `DriverManagerDataSource`: + +include-code::./DriverManagerDataSourceConfiguration[tag=snippet,indent=0] The next two examples show the basic connectivity and configuration for DBCP and C3P0. To learn about more options that help control the pooling features, see the product @@ -94,32 +56,11 @@ documentation for the respective connection pooling implementations. The following example shows DBCP configuration: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- +include-code::./BasicDataSourceConfiguration[tag=snippet,indent=0] The following example shows C3P0 configuration: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- - +include-code::./ComboPooledDataSourceConfiguration[tag=snippet,indent=0] [[jdbc-DataSourceUtils]] == Using `DataSourceUtils` @@ -199,7 +140,7 @@ participating in Spring managed transactions. It is generally preferable to writ own new code by using the higher level abstractions for resource management, such as `JdbcTemplate` or `DataSourceUtils`. -See the {api-spring-framework}/jdbc/datasource/TransactionAwareDataSourceProxy.html[`TransactionAwareDataSourceProxy`] +See the {spring-framework-api}/jdbc/datasource/TransactionAwareDataSourceProxy.html[`TransactionAwareDataSourceProxy`] javadoc for more details. @@ -230,6 +171,19 @@ provided you stick to the required connection lookup pattern. Note that JTA does savepoints or custom isolation levels and has a different timeout mechanism but otherwise exposes similar behavior in terms of JDBC resources and JDBC commit/rollback management. +For JTA-style lazy retrieval of actual resource connections, Spring provides a +corresponding `DataSource` proxy class for the target connection pool: see +{spring-framework-api}/jdbc/datasource/LazyConnectionDataSourceProxy.html[`LazyConnectionDataSourceProxy`]. +This is particularly useful for potentially empty transactions without actual statement +execution (never fetching an actual resource in such a scenario), and also in front of +a routing `DataSource` which means to take the transaction-synchronized read-only flag +and/or isolation level into account (for example, `IsolationLevelDataSourceRouter`). + +`LazyConnectionDataSourceProxy` also provides special support for a read-only connection +pool to use during a read-only transaction, avoiding the overhead of switching the JDBC +Connection's read-only flag at the beginning and end of every transaction when fetching +it from the primary connection pool (which may be costly depending on the JDBC driver). + NOTE: As of 5.3, Spring provides an extended `JdbcTransactionManager` variant which adds exception translation capabilities on commit/rollback (aligned with `JdbcTemplate`). Where `DataSourceTransactionManager` will only ever throw `TransactionSystemException` diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc index 60b6eea8fb96..7f4eaf6be896 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc @@ -51,7 +51,7 @@ corresponding to the fully qualified class name of the template instance (typica The following sections provide some examples of `JdbcTemplate` usage. These examples are not an exhaustive list of all of the functionality exposed by the `JdbcTemplate`. -See the attendant {api-spring-framework}/jdbc/core/JdbcTemplate.html[javadoc] for that. +See the attendant {spring-framework-api}/jdbc/core/JdbcTemplate.html[javadoc] for that. [[jdbc-JdbcTemplate-examples-query]] === Querying (`SELECT`) @@ -62,14 +62,14 @@ The following query gets the number of rows in a relation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor")!! ---- @@ -81,7 +81,7 @@ The following query uses a bind variable: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject( "select count(*) from t_actor where first_name = ?", Integer.class, "Joe"); @@ -89,7 +89,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val countOfActorsNamedJoe = jdbcTemplate.queryForObject( "select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!! @@ -103,7 +103,7 @@ The following query looks for a `String`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String lastName = this.jdbcTemplate.queryForObject( "select last_name from t_actor where id = ?", @@ -112,7 +112,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val lastName = this.jdbcTemplate.queryForObject( "select last_name from t_actor where id = ?", @@ -126,7 +126,7 @@ The following query finds and populates a single domain object: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Actor actor = jdbcTemplate.queryForObject( "select first_name, last_name from t_actor where id = ?", @@ -141,7 +141,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val actor = jdbcTemplate.queryForObject( "select first_name, last_name from t_actor where id = ?", @@ -157,7 +157,7 @@ The following query finds and populates a list of domain objects: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- List actors = this.jdbcTemplate.query( "select first_name, last_name from t_actor", @@ -171,7 +171,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val actors = jdbcTemplate.query("select first_name, last_name from t_actor") { rs, _ -> Actor(rs.getString("first_name"), rs.getString("last_name")) @@ -187,7 +187,7 @@ For example, it may be better to write the preceding code snippet as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- private final RowMapper actorRowMapper = (resultSet, rowNum) -> { Actor actor = new Actor(); @@ -203,7 +203,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val actorMapper = RowMapper { rs: ResultSet, rowNum: Int -> Actor(rs.getString("first_name"), rs.getString("last_name")) @@ -227,7 +227,7 @@ The following example inserts a new entry: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- this.jdbcTemplate.update( "insert into t_actor (first_name, last_name) values (?, ?)", @@ -236,7 +236,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- jdbcTemplate.update( "insert into t_actor (first_name, last_name) values (?, ?)", @@ -250,7 +250,7 @@ The following example updates an existing entry: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- this.jdbcTemplate.update( "update t_actor set last_name = ? where id = ?", @@ -259,7 +259,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- jdbcTemplate.update( "update t_actor set last_name = ? where id = ?", @@ -273,7 +273,7 @@ The following example deletes an entry: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- this.jdbcTemplate.update( "delete from t_actor where id = ?", @@ -282,7 +282,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong()) ---- @@ -300,14 +300,14 @@ table: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- jdbcTemplate.execute("create table mytable (id integer, name varchar(100))") ---- @@ -319,7 +319,7 @@ The following example invokes a stored procedure: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- this.jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", @@ -328,7 +328,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", @@ -339,7 +339,7 @@ Kotlin:: More sophisticated stored procedure support is xref:data-access/jdbc/object.adoc#jdbc-StoredProcedure[covered later]. -[[jdbc-JdbcTemplate-idioms]] +[[jdbc-jdbctemplate-idioms]] === `JdbcTemplate` Best Practices Instances of the `JdbcTemplate` class are thread-safe, once configured. This is @@ -352,147 +352,23 @@ A common practice when using the `JdbcTemplate` class (and the associated xref:data-access/jdbc/core.adoc#jdbc-NamedParameterJdbcTemplate[`NamedParameterJdbcTemplate`] class) is to configure a `DataSource` in your Spring configuration file and then dependency-inject that shared `DataSource` bean into your DAO classes. The `JdbcTemplate` is created in -the setter for the `DataSource`. This leads to DAOs that resemble the following: +the setter for the `DataSource` or in the constructor. This leads to DAOs that resemble the following: --- -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class JdbcCorporateEventDao implements CorporateEventDao { +include-code::./JdbcCorporateEventDao[tag=snippet,indent=0] - private JdbcTemplate jdbcTemplate; - - public void setDataSource(DataSource dataSource) { - this.jdbcTemplate = new JdbcTemplate(dataSource); - } +The following example shows the corresponding configuration: - // JDBC-backed implementations of the methods on the CorporateEventDao follow... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { - - private val jdbcTemplate = JdbcTemplate(dataSource) - - // JDBC-backed implementations of the methods on the CorporateEventDao follow... - } ----- -====== --- - -The following example shows the corresponding XML configuration: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - - - - ----- +include-code::./JdbcCorporateEventDaoConfiguration[tag=snippet,indent=0] An alternative to explicit configuration is to use component-scanning and annotation support for dependency injection. In this case, you can annotate the class with `@Repository` -(which makes it a candidate for component-scanning) and annotate the `DataSource` setter -method with `@Autowired`. The following example shows how to do so: +(which makes it a candidate for component-scanning). The following example shows how to do so: --- -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Repository // <1> - public class JdbcCorporateEventDao implements CorporateEventDao { - - private JdbcTemplate jdbcTemplate; - - @Autowired // <2> - public void setDataSource(DataSource dataSource) { - this.jdbcTemplate = new JdbcTemplate(dataSource); // <3> - } - - // JDBC-backed implementations of the methods on the CorporateEventDao follow... - } ----- -<1> Annotate the class with `@Repository`. -<2> Annotate the `DataSource` setter method with `@Autowired`. -<3> Create a new `JdbcTemplate` with the `DataSource`. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Repository // <1> - class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { // <2> - - private val jdbcTemplate = JdbcTemplate(dataSource) // <3> - - // JDBC-backed implementations of the methods on the CorporateEventDao follow... - } ----- -<1> Annotate the class with `@Repository`. -<2> Constructor injection of the `DataSource`. -<3> Create a new `JdbcTemplate` with the `DataSource`. -====== --- - - -The following example shows the corresponding XML configuration: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - +include-code::./JdbcCorporateEventRepository[tag=snippet,indent=0] - - - - - - +The following example shows the corresponding configuration: - - - ----- +include-code::./JdbcCorporateEventRepositoryConfiguration[tag=snippet,indent=0] If you use Spring's `JdbcDaoSupport` class and your various JDBC-backed DAO classes extend from it, your sub-class inherits a `setDataSource(..)` method from the @@ -522,7 +398,7 @@ parameters. The following example shows how to use `NamedParameterJdbcTemplate`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @@ -540,7 +416,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) @@ -567,7 +443,7 @@ The following example shows the use of the `Map`-based style: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @@ -585,7 +461,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // some JDBC-backed DAO class... private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) @@ -618,7 +494,7 @@ The following example shows a typical JavaBean: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class Actor { @@ -644,7 +520,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- data class Actor(val id: Long, val firstName: String, val lastName: String) ---- @@ -657,7 +533,7 @@ members of the class shown in the preceding example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @@ -676,7 +552,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // some JDBC-backed DAO class... private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) @@ -698,7 +574,7 @@ functionality that is present only in the `JdbcTemplate` class, you can use the `getJdbcOperations()` method to access the wrapped `JdbcTemplate` through the `JdbcOperations` interface. -See also xref:data-access/jdbc/core.adoc#jdbc-JdbcTemplate-idioms[`JdbcTemplate` Best Practices] +See also xref:data-access/jdbc/core.adoc#jdbc-jdbctemplate-idioms[`JdbcTemplate` Best Practices] for guidelines on using the `NamedParameterJdbcTemplate` class in the context of an application. @@ -717,7 +593,7 @@ For example, with positional parameters: public int countOfActorsByFirstName(String firstName) { return this.jdbcClient.sql("select count(*) from t_actor where first_name = ?") - .param(firstName); + .param(firstName) .query(Integer.class).single(); } ---- @@ -730,7 +606,7 @@ For example, with named parameters: public int countOfActorsByFirstName(String firstName) { return this.jdbcClient.sql("select count(*) from t_actor where first_name = :firstName") - .param("firstName", firstName); + .param("firstName", firstName) .query(Integer.class).single(); } ---- @@ -759,8 +635,8 @@ With a required single object result: [source,java,indent=0,subs="verbatim,quotes"] ---- - Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?", - .param(1212L); + Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?") + .param(1212L) .query(Actor.class) .single(); ---- @@ -769,8 +645,8 @@ With a `java.util.Optional` result: [source,java,indent=0,subs="verbatim,quotes"] ---- - Optional actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?", - .param(1212L); + Optional actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?") + .param(1212L) .query(Actor.class) .optional(); ---- @@ -780,7 +656,7 @@ And for an update statement: [source,java,indent=0,subs="verbatim,quotes"] ---- this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)") - .param("Leonor").param("Watling"); + .param("Leonor").param("Watling") .update(); ---- @@ -789,7 +665,7 @@ Or an update statement with named parameters: [source,java,indent=0,subs="verbatim,quotes"] ---- this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)") - .param("firstName", "Leonor").param("lastName", "Watling"); + .param("firstName", "Leonor").param("lastName", "Watling") .update(); ---- @@ -800,7 +676,7 @@ provides `firstName` and `lastName` properties, such as the `Actor` class from a [source,java,indent=0,subs="verbatim,quotes"] ---- this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)") - .paramSource(new Actor("Leonor", "Watling"); + .paramSource(new Actor("Leonor", "Watling") .update(); ---- @@ -870,7 +746,7 @@ You can extend `SQLErrorCodeSQLExceptionTranslator`, as the following example sh ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator { @@ -885,7 +761,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() { @@ -910,7 +786,7 @@ how you can use this custom translator: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- private JdbcTemplate jdbcTemplate; @@ -935,7 +811,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // create a JdbcTemplate and set data source private val jdbcTemplate = JdbcTemplate(dataSource).apply { @@ -970,7 +846,7 @@ fully functional class that creates a new table: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; @@ -991,7 +867,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import javax.sql.DataSource import org.springframework.jdbc.core.JdbcTemplate @@ -1021,7 +897,7 @@ query methods, one for an `int` and one that queries for a `String`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; @@ -1046,7 +922,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import javax.sql.DataSource import org.springframework.jdbc.core.JdbcTemplate @@ -1074,7 +950,7 @@ list of all the rows, it might be as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- private JdbcTemplate jdbcTemplate; @@ -1089,7 +965,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- private val jdbcTemplate = JdbcTemplate(dataSource) @@ -1116,7 +992,7 @@ The following example updates a column for a certain primary key: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; @@ -1137,7 +1013,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import javax.sql.DataSource import org.springframework.jdbc.core.JdbcTemplate @@ -1175,7 +1051,7 @@ on Oracle but may not work on other platforms: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- final String INSERT_SQL = "insert into my_test (name) values(?)"; final String name = "Rob"; @@ -1192,7 +1068,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val INSERT_SQL = "insert into my_test (name) values(?)" val name = "Rob" 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 eaa86da5f268..96a6023dac51 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 @@ -16,124 +16,22 @@ lightweight nature. Benefits include ease of configuration, quick startup time, testability, and the ability to rapidly evolve your SQL during development. -[[jdbc-embedded-database-xml]] -== Creating an Embedded Database by Using Spring XML +[[jdbc-embedded-database]] +== Creating an Embedded Database -If you want to expose an embedded database instance as a bean in a Spring -`ApplicationContext`, you can use the `embedded-database` tag in the `spring-jdbc` namespace: +You can expose an embedded database instance as a bean as the following example shows: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - ----- +include-code::./JdbcEmbeddedDatabaseConfiguration[tag=snippet,indent=0] -The preceding configuration creates an embedded HSQL database that is populated with SQL from +The preceding configuration creates an embedded H2 database that is populated with SQL from the `schema.sql` and `test-data.sql` resources in the root of the classpath. In addition, as a best practice, the embedded database is assigned a uniquely generated name. The embedded database is made available to the Spring container as a bean of type `javax.sql.DataSource` that can then be injected into data access objects as needed. - -[[jdbc-embedded-database-java]] -== Creating an Embedded Database Programmatically - -The `EmbeddedDatabaseBuilder` class provides a fluent API for constructing an embedded -database programmatically. You can use this when you need to create an embedded database in a -stand-alone environment or in a stand-alone integration test, as in the following example: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - EmbeddedDatabase db = new EmbeddedDatabaseBuilder() - .generateUniqueName(true) - .setType(H2) - .setScriptEncoding("UTF-8") - .ignoreFailedDrops(true) - .addScript("schema.sql") - .addScripts("user_data.sql", "country_data.sql") - .build(); - - // perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource) - - db.shutdown() ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val db = EmbeddedDatabaseBuilder() - .generateUniqueName(true) - .setType(H2) - .setScriptEncoding("UTF-8") - .ignoreFailedDrops(true) - .addScript("schema.sql") - .addScripts("user_data.sql", "country_data.sql") - .build() - - // perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource) - - db.shutdown() ----- -====== - -See the {api-spring-framework}/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.html[javadoc for `EmbeddedDatabaseBuilder`] +See the {spring-framework-api}/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.html[javadoc for `EmbeddedDatabaseBuilder`] for further details on all supported options. -You can also use the `EmbeddedDatabaseBuilder` to create an embedded database by using Java -configuration, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - public class DataSourceConfig { - - @Bean - public DataSource dataSource() { - return new EmbeddedDatabaseBuilder() - .generateUniqueName(true) - .setType(H2) - .setScriptEncoding("UTF-8") - .ignoreFailedDrops(true) - .addScript("schema.sql") - .addScripts("user_data.sql", "country_data.sql") - .build(); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - class DataSourceConfig { - - @Bean - fun dataSource(): DataSource { - return EmbeddedDatabaseBuilder() - .generateUniqueName(true) - .setType(H2) - .setScriptEncoding("UTF-8") - .ignoreFailedDrops(true) - .addScript("schema.sql") - .addScripts("user_data.sql", "country_data.sql") - .build() - } - } ----- -====== - [[jdbc-embedded-database-types]] == Selecting the Embedded Database Type @@ -168,6 +66,74 @@ attribute of the `embedded-database` tag to `DERBY`. If you use the builder API, call the `setType(EmbeddedDatabaseType)` method with `EmbeddedDatabaseType.DERBY`. +[[jdbc-embedded-database-types-custom]] +== Customizing the Embedded Database Type + +While each supported type comes with default connection settings, it is possible +to customize them if necessary. The following example uses H2 with a custom driver: + +[tabs] +====== +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); + } + }; + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Configuration + class DataSourceConfig { + + @Bean + fun dataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .setDatabaseConfigurer(EmbeddedDatabaseConfigurers + .customizeConfigurer(EmbeddedDatabaseType.H2) { this.customize(it) }) + .addScript("schema.sql") + .build() + } + + private fun customize(defaultConfigurer: EmbeddedDatabaseConfigurer): EmbeddedDatabaseConfigurer { + return object : EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + override fun configureConnectionProperties( + properties: ConnectionProperties, + databaseName: String + ) { + super.configureConnectionProperties(properties, databaseName) + properties.setDriverClass(CustomDriver::class.java) + } + } + } + } +---- +====== + + [[jdbc-embedded-database-dao-testing]] == Testing Data Access Logic with an Embedded Database @@ -177,14 +143,14 @@ can be useful for one-offs when the embedded database does not need to be reused classes. However, if you wish to create an embedded database that is shared within a test suite, consider using the xref:testing/testcontext-framework.adoc[Spring TestContext Framework] and configuring the embedded database as a bean in the Spring `ApplicationContext` as described -in xref:data-access/jdbc/embedded-database-support.adoc#jdbc-embedded-database-xml[Creating an Embedded Database by Using Spring XML] and xref:data-access/jdbc/embedded-database-support.adoc#jdbc-embedded-database-java[Creating an Embedded Database Programmatically]. The following listing -shows the test template: +in xref:data-access/jdbc/embedded-database-support.adoc#jdbc-embedded-database[Creating an Embedded Database]. +The following listing shows the test template: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class DataAccessIntegrationTestTemplate { @@ -216,7 +182,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class DataAccessIntegrationTestTemplate { @@ -288,7 +254,7 @@ You can extend Spring JDBC embedded database support in two ways: connection pool to manage embedded database connections. We encourage you to contribute extensions to the Spring community at -https://github.com/spring-projects/spring-framework/issues[GitHub Issues]. +{spring-framework-issues}[GitHub Issues]. diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/object.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/object.adoc index 65fd60c76e05..dc3c135ae525 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/object.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/object.adoc @@ -44,7 +44,7 @@ data from the `t_actor` relation to an instance of the `Actor` class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ActorMappingQuery extends MappingSqlQuery { @@ -67,7 +67,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ActorMappingQuery(ds: DataSource) : MappingSqlQuery(ds, "select id, first_name, last_name from t_actor where id = ?") { @@ -103,7 +103,7 @@ example shows how to define such a class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- private ActorMappingQuery actorMappingQuery; @@ -112,22 +112,22 @@ Java:: this.actorMappingQuery = new ActorMappingQuery(dataSource); } - public Customer getCustomer(Long id) { + public Actor getActor(Long id) { return actorMappingQuery.findObject(id); } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- private val actorMappingQuery = ActorMappingQuery(dataSource) - fun getCustomer(id: Long) = actorMappingQuery.findObject(id) + fun getActor(id: Long) = actorMappingQuery.findObject(id) ---- ====== -The method in the preceding example retrieves the customer with the `id` that is passed in as the +The method in the preceding example retrieves the actor with the `id` that is passed in as the only parameter. Since we want only one object to be returned, we call the `findObject` convenience method with the `id` as the parameter. If we had instead a query that returned a list of objects and took additional parameters, we would use one of the `execute` @@ -138,7 +138,7 @@ example shows such a method: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public List searchForActors(int age, String namePattern) { return actorSearchMappingQuery.execute(age, namePattern); @@ -147,7 +147,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun searchForActors(age: Int, namePattern: String) = actorSearchMappingQuery.execute(age, namePattern) @@ -171,7 +171,7 @@ The following example creates a custom update method named `execute`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import java.sql.Types; import javax.sql.DataSource; @@ -201,7 +201,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import java.sql.Types import javax.sql.DataSource @@ -247,7 +247,7 @@ as the following code snippet shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- new SqlParameter("in_id", Types.NUMERIC), new SqlOutParameter("out_first_name", Types.VARCHAR), @@ -255,7 +255,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- SqlParameter("in_id", Types.NUMERIC), SqlOutParameter("out_first_name", Types.VARCHAR), @@ -293,7 +293,7 @@ The following listing shows our custom StoredProcedure class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import java.sql.Types; import java.util.Date; @@ -342,7 +342,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import java.sql.Types import java.util.Date @@ -387,7 +387,7 @@ Oracle REF cursors): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import java.util.HashMap; import java.util.Map; @@ -416,7 +416,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import java.util.HashMap import javax.sql.DataSource @@ -456,7 +456,7 @@ the supplied `ResultSet`, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import java.sql.ResultSet; import java.sql.SQLException; @@ -476,7 +476,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import java.sql.ResultSet import com.foo.domain.Title @@ -497,7 +497,7 @@ the supplied `ResultSet`, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import java.sql.ResultSet; import java.sql.SQLException; @@ -514,7 +514,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import java.sql.ResultSet import com.foo.domain.Genre @@ -537,7 +537,7 @@ delegate to the untyped `execute(Map)` method in the superclass, as the followin ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import java.sql.Types; import java.util.Date; @@ -571,7 +571,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import java.sql.Types import java.util.Date diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/parameter-handling.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/parameter-handling.adoc index b0035ea95466..3a9edff0ccbd 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/parameter-handling.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/parameter-handling.adoc @@ -67,7 +67,7 @@ The following example shows how to create and insert a BLOB: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- final File blobIn = new File("spring2004.jpg"); final InputStream blobIs = new FileInputStream(blobIn); @@ -95,7 +95,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val blobIn = File("spring2004.jpg") val blobIs = FileInputStream(blobIn) @@ -142,7 +142,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- List> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table", new RowMapper>() { @@ -161,7 +161,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table") { rs, _ -> val clobText = lobHandler.getClobAsString(rs, "a_clob") // <1> @@ -209,141 +209,26 @@ are passed in as a parameter to the stored procedure. The `SqlReturnType` interface has a single method (named `getTypeValue`) that must be implemented. This interface is used as part of the declaration of an `SqlOutParameter`. -The following example shows returning the value of an Oracle `STRUCT` object of the user +The following example shows returning the value of a `java.sql.Struct` object of the user declared type `ITEM_TYPE`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class TestItemStoredProcedure extends StoredProcedure { - - public TestItemStoredProcedure(DataSource dataSource) { - // ... - declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE", - (CallableStatement cs, int colIndx, int sqlType, String typeName) -> { - STRUCT struct = (STRUCT) cs.getObject(colIndx); - Object[] attr = struct.getAttributes(); - TestItem item = new TestItem(); - item.setId(((Number) attr[0]).longValue()); - item.setDescription((String) attr[1]); - item.setExpirationDate((java.util.Date) attr[2]); - return item; - })); - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() { - - init { - // ... - declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName -> - val struct = cs.getObject(colIndx) as STRUCT - val attr = struct.getAttributes() - TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date) - }) - // ... - } - } ----- -====== +include-code::./TestItemStoredProcedure[] You can use `SqlTypeValue` to pass the value of a Java object (such as `TestItem`) to a stored procedure. The `SqlTypeValue` interface has a single method (named `createTypeValue`) that you must implement. The active connection is passed in, and you -can use it to create database-specific objects, such as `StructDescriptor` instances -or `ArrayDescriptor` instances. The following example creates a `StructDescriptor` instance: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - final TestItem testItem = new TestItem(123L, "A test item", - new SimpleDateFormat("yyyy-M-d").parse("2010-12-31")); - - SqlTypeValue value = new AbstractSqlTypeValue() { - protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { - StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn); - Struct item = new STRUCT(itemDescriptor, conn, - new Object[] { - testItem.getId(), - testItem.getDescription(), - new java.sql.Date(testItem.getExpirationDate().getTime()) - }); - return item; - } - }; ----- +can use it to create database-specific objects, such as `java.sql.Struct` instances +or `java.sql.Array` instances. The following example creates a `java.sql.Struct` instance: -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val (id, description, expirationDate) = TestItem(123L, "A test item", - SimpleDateFormat("yyyy-M-d").parse("2010-12-31")) - - val value = object : AbstractSqlTypeValue() { - override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { - val itemDescriptor = StructDescriptor(typeName, conn) - return STRUCT(itemDescriptor, conn, - arrayOf(id, description, java.sql.Date(expirationDate.time))) - } - } ----- -====== +include-code::./SqlTypeValueFactory[tag=struct,indent=0] You can now add this `SqlTypeValue` to the `Map` that contains the input parameters for the `execute` call of the stored procedure. Another use for the `SqlTypeValue` is passing in an array of values to an Oracle stored -procedure. Oracle has its own internal `ARRAY` class that must be used in this case, and -you can use the `SqlTypeValue` to create an instance of the Oracle `ARRAY` and populate -it with values from the Java `ARRAY`, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - final Long[] ids = new Long[] {1L, 2L}; - - SqlTypeValue value = new AbstractSqlTypeValue() { - protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { - ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn); - ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids); - return idArray; - } - }; ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() { - - init { - val ids = arrayOf(1L, 2L) - val value = object : AbstractSqlTypeValue() { - override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { - val arrayDescriptor = ArrayDescriptor(typeName, conn) - return ARRAY(arrayDescriptor, conn, ids) - } - } - } - } ----- -====== - +procedure. Oracle has an `createOracleArray` method on `OracleConnection` that you can +access by unwrapping it. You can use the `SqlTypeValue` to create an array and populate +it with values from the Java `java.sql.Array`, as the following example shows: +include-code::./SqlTypeValueFactory[tag=oracle-array,indent=0] diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc index 9e0c0200a179..27c3e6898352 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc @@ -23,7 +23,7 @@ example uses only one configuration method (we show examples of multiple methods ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -47,7 +47,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -85,7 +85,7 @@ listing shows how it works: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -111,7 +111,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -150,7 +150,7 @@ You can limit the columns for an insert by specifying a list of column names wit ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -177,7 +177,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -217,7 +217,7 @@ values. The following example shows how to use `BeanPropertySqlParameterSource`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -241,7 +241,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -267,7 +267,7 @@ convenient `addValue` method that can be chained. The following example shows ho ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -293,7 +293,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -323,12 +323,12 @@ use these alternative input classes. The `SimpleJdbcCall` class uses metadata in the database to look up names of `in` and `out` parameters so that you do not have to explicitly declare them. You can -declare parameters if you prefer to do that or if you have parameters (such as `ARRAY` -or `STRUCT`) that do not have an automatic mapping to a Java class. The first example -shows a simple procedure that returns only scalar values in `VARCHAR` and `DATE` format -from a MySQL database. The example procedure reads a specified actor entry and returns -`first_name`, `last_name`, and `birth_date` columns in the form of `out` parameters. -The following listing shows the first example: +declare parameters if you prefer to do that or if you have parameters that do not +have an automatic mapping to a Java class. The first example shows a simple procedure +that returns only scalar values in `VARCHAR` and `DATE` format from a MySQL database. +The example procedure reads a specified actor entry and returns `first_name`, +`last_name`, and `birth_date` columns in the form of `out` parameters. The following +listing shows the first example: [source,sql,indent=0,subs="verbatim,quotes"] ---- @@ -359,7 +359,7 @@ of the stored procedure): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -388,7 +388,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -437,7 +437,7 @@ the constructor of your `SimpleJdbcCall`. The following example shows this confi ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -456,7 +456,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -502,7 +502,7 @@ the preceding example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -529,7 +529,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -567,7 +567,7 @@ similar to the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- new SqlParameter("in_id", Types.NUMERIC), new SqlOutParameter("out_first_name", Types.VARCHAR), @@ -575,7 +575,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- SqlParameter("in_id", Types.NUMERIC), SqlOutParameter("out_first_name", Types.VARCHAR), @@ -636,7 +636,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -662,7 +662,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { @@ -720,7 +720,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class JdbcActorDao implements ActorDao { @@ -746,7 +746,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class JdbcActorDao(dataSource: DataSource) : ActorDao { diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/general.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/general.adoc index 3138409d36e3..bbbd6b8b4542 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/general.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/general.adoc @@ -65,7 +65,7 @@ examples (one for Java configuration and one for XML configuration) show how to ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Repository public class ProductDaoImpl implements ProductDao { @@ -77,7 +77,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repository class ProductDaoImpl : ProductDao { diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc index a782b9165e01..149841aeb3e9 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc @@ -87,8 +87,8 @@ On `LocalSessionFactoryBean`, this is available through the `bootstrapExecutor` property. On the programmatic `LocalSessionFactoryBuilder`, there is an overloaded `buildSessionFactory` method that takes a bootstrap executor argument. -As of Spring Framework 5.1, such a native Hibernate setup can also expose a JPA -`EntityManagerFactory` for standard JPA interaction next to native Hibernate access. +Such a native Hibernate setup can also expose a JPA `EntityManagerFactory` for standard +JPA interaction next to native Hibernate access. See xref:data-access/orm/jpa.adoc#orm-jpa-hibernate[Native Hibernate Setup for JPA] for details. ==== @@ -105,7 +105,7 @@ implementation resembles the following example, based on the plain Hibernate API ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ProductDaoImpl implements ProductDao { @@ -126,7 +126,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao { @@ -208,7 +208,7 @@ these annotated methods. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ProductServiceImpl implements ProductService { @@ -233,7 +233,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ProductServiceImpl(private val productDao: ProductDao) : ProductService { @@ -317,7 +317,7 @@ and an example for a business method implementation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ProductServiceImpl implements ProductService { @@ -345,7 +345,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ProductServiceImpl(transactionManager: PlatformTransactionManager, private val productDao: ProductDao) : ProductService { @@ -407,6 +407,12 @@ exposes the Hibernate transaction as a JDBC transaction if you have set up the p `DataSource` for which the transactions are supposed to be exposed through the `dataSource` property of the `HibernateTransactionManager` class. +For JTA-style lazy retrieval of actual resource connections, Spring provides a +corresponding `DataSource` proxy class for the target connection pool: see +{spring-framework-api}/jdbc/datasource/LazyConnectionDataSourceProxy.html[`LazyConnectionDataSourceProxy`]. +This is particularly useful for Hibernate read-only transactions which can often +be processed from a local cache rather than hitting the database. + [[orm-hibernate-resources]] == Comparing Container-managed and Locally Defined Resources diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc index bfb3e3c532ac..d44aca0c20d7 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc @@ -58,8 +58,8 @@ The benefits of using the Spring Framework to create your ORM DAOs include: TIP: For more comprehensive ORM support, including support for alternative database technologies such as MongoDB, you might want to check out the -https://projects.spring.io/spring-data/[Spring Data] suite of projects. If you are -a JPA user, the https://spring.io/guides/gs/accessing-data-jpa/[Getting Started Accessing +{spring-site-projects}/spring-data/[Spring Data] suite of projects. If you are +a JPA user, the {spring-site-guides}/gs/accessing-data-jpa/[Getting Started Accessing Data with JPA] guide from https://spring.io provides a great introduction. diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc index b9fc4279fc3f..011f1033ad0c 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc @@ -142,8 +142,7 @@ Alternatively, specify a custom `persistenceXmlLocation` on your META-INF/my-persistence.xml) and include only a descriptor with that name in your application jar files. Because the Jakarta EE server looks only for default `META-INF/persistence.xml` files, it ignores such custom persistence units and, hence, -avoids conflicts with a Spring-driven JPA setup upfront. (This applies to Resin 3.1, for -example.) +avoids conflicts with a Spring-driven JPA setup upfront. .When is load-time weaving required? **** @@ -157,7 +156,7 @@ The `LoadTimeWeaver` interface is a Spring-provided class that lets JPA `ClassTransformer` instances be plugged in a specific manner, depending on whether the environment is a web container or application server. Hooking `ClassTransformers` through an -https://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html[agent] +{java-api}/java.instrument/java/lang/instrument/package-summary.html[agent] is typically not efficient. The agents work against the entire virtual machine and inspect every class that is loaded, which is usually undesirable in a production server environment. @@ -175,7 +174,7 @@ a context-wide `LoadTimeWeaver` by using the `@EnableLoadTimeWeaving` annotation `context:load-time-weaver` XML element. Such a global weaver is automatically picked up by all JPA `LocalContainerEntityManagerFactoryBean` instances. The following example shows the preferred way of setting up a load-time weaver, delivering auto-detection -of the platform (e.g. Tomcat's weaving-capable class loader or Spring's JVM agent) +of the platform (for example, Tomcat's weaving-capable class loader or Spring's JVM agent) and automatic propagation of the weaver to all weaver-aware beans: [source,xml,indent=0,subs="verbatim,quotes"] @@ -268,10 +267,17 @@ The actual JPA provider bootstrapping is handed off to the specified executor an running in parallel, to the application bootstrap thread. The exposed `EntityManagerFactory` proxy can be injected into other application components and is even able to respond to `EntityManagerFactoryInfo` configuration inspection. However, once the actual JPA provider -is being accessed by other components (for example, calling `createEntityManager`), those calls -block until the background bootstrapping has completed. In particular, when you use +is being accessed by other components (for example, calling `createEntityManager`), those +calls block until the background bootstrapping has completed. In particular, when you use Spring Data JPA, make sure to set up deferred bootstrapping for its repositories as well. +As of 6.2, JPA initialization is enforced before context refresh completion, waiting for +asynchronous bootstrapping to complete by then. This makes the availability of the fully +initialized database infrastructure predictable and allows for custom post-initialization +logic in `ContextRefreshedEvent` listeners etc. Putting such application-level database +initialization into `@PostConstruct` methods or the like is not recommended; this is +better placed in `Lifecycle.start` (if applicable) or a `ContextRefreshedEvent` listener. + [[orm-jpa-dao]] == Implementing DAOs Based on JPA: `EntityManagerFactory` and `EntityManager` @@ -284,15 +290,15 @@ to a newly created `EntityManager` per operation, in effect making its usage thr It is possible to write code against the plain JPA without any Spring dependencies, by using an injected `EntityManagerFactory` or `EntityManager`. Spring can understand the -`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method level -if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example shows a plain -JPA DAO implementation that uses the `@PersistenceUnit` annotation: +`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method +level if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example +shows a plain JPA DAO implementation that uses the `@PersistenceUnit` annotation: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ProductDaoImpl implements ProductDao { @@ -321,7 +327,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ProductDaoImpl : ProductDao { @@ -387,7 +393,7 @@ EntityManager) to be injected instead of the factory. The following example show ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ProductDaoImpl implements ProductDao { @@ -404,7 +410,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ProductDaoImpl : ProductDao { @@ -462,7 +468,7 @@ a non-invasiveness perspective and can feel more natural to JPA developers. What about providing JPA resources via constructors and other `@Autowired` injection points? `EntityManagerFactory` can easily be injected via constructors and `@Autowired` fields/methods -as long as the target is defined as a bean, e.g. via `LocalContainerEntityManagerFactoryBean`. +as long as the target is defined as a bean, for example, via `LocalContainerEntityManagerFactoryBean`. The injection point matches the original `EntityManagerFactory` definition by type as-is. However, an `@PersistenceContext`-style shared `EntityManager` reference is not available for @@ -506,13 +512,20 @@ if you have not already done so, to get more detailed coverage of Spring's decla The recommended strategy for JPA is local transactions through JPA's native transaction support. Spring's `JpaTransactionManager` provides many capabilities known from local JDBC transactions (such as transaction-specific isolation levels and resource-level -read-only optimizations) against any regular JDBC connection pool (no XA requirement). +read-only optimizations) against any regular JDBC connection pool, without requiring +a JTA transaction coordinator and XA-capable resources. Spring JPA also lets a configured `JpaTransactionManager` expose a JPA transaction to JDBC access code that accesses the same `DataSource`, provided that the registered -`JpaDialect` supports retrieval of the underlying JDBC `Connection`. -Spring provides dialects for the EclipseLink and Hibernate JPA implementations. -See the xref:data-access/orm/jpa.adoc#orm-jpa-dialect[next section] for details on the `JpaDialect` mechanism. +`JpaDialect` supports retrieval of the underlying JDBC `Connection`. Spring provides +dialects for the EclipseLink and Hibernate JPA implementations. See the +xref:data-access/orm/jpa.adoc#orm-jpa-dialect[next section] for details on `JpaDialect`. + +For JTA-style lazy retrieval of actual resource connections, Spring provides a +corresponding `DataSource` proxy class for the target connection pool: see +{spring-framework-api}/jdbc/datasource/LazyConnectionDataSourceProxy.html[`LazyConnectionDataSourceProxy`]. +This is particularly useful for JPA read-only transactions which can often +be processed from a local cache rather than hitting the database. [[orm-jpa-dialect]] @@ -541,8 +554,8 @@ way of auto-configuring an `EntityManagerFactory` setup for Hibernate or Eclipse respectively. Note that those provider adapters are primarily designed for use with Spring-driven transaction management (that is, for use with `JpaTransactionManager`). -See the {api-spring-framework}/orm/jpa/JpaDialect.html[`JpaDialect`] and -{api-spring-framework}/orm/jpa/JpaVendorAdapter.html[`JpaVendorAdapter`] javadoc for +See the {spring-framework-api}/orm/jpa/JpaDialect.html[`JpaDialect`] and +{spring-framework-api}/orm/jpa/JpaVendorAdapter.html[`JpaVendorAdapter`] javadoc for more details of its operations and how they are used within Spring's JPA support. diff --git a/framework-docs/modules/ROOT/pages/data-access/oxm.adoc b/framework-docs/modules/ROOT/pages/data-access/oxm.adoc index 3cbe3a502e70..3296f532933e 100644 --- a/framework-docs/modules/ROOT/pages/data-access/oxm.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/oxm.adoc @@ -36,8 +36,8 @@ simpler. [[oxm-consistent-interfaces]] === Consistent Interfaces -Spring's O-X mapping operates through two global interfaces: {api-spring-framework}/oxm/Marshaller.html[`Marshaller`] and -{api-spring-framework}/oxm/Unmarshaller.html[`Unmarshaller`]. These abstractions let you switch O-X mapping frameworks +Spring's O-X mapping operates through two global interfaces: {spring-framework-api}/oxm/Marshaller.html[`Marshaller`] and +{spring-framework-api}/oxm/Unmarshaller.html[`Unmarshaller`]. These abstractions let you switch O-X mapping frameworks with relative ease, with little or no change required on the classes that do the marshalling. This approach has the additional benefit of making it possible to do XML marshalling with a mix-and-match approach (for example, some marshalling performed using JAXB @@ -175,7 +175,7 @@ use a simple JavaBean to represent the settings: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class Settings { @@ -193,7 +193,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Settings { var isFooEnabled: Boolean = false @@ -210,7 +210,7 @@ constructs a Spring application context and calls these two methods: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import java.io.FileInputStream; import java.io.FileOutputStream; @@ -261,7 +261,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Application { @@ -557,7 +557,7 @@ set the `supportedClasses` property on the `XStreamMarshaller`, as the following Doing so ensures that only the registered classes are eligible for unmarshalling. Additionally, you can register -{api-spring-framework}/oxm/xstream/XStreamMarshaller.html#setConverters(com.thoughtworks.xstream.converters.ConverterMatcher...)[custom +{spring-framework-api}/oxm/xstream/XStreamMarshaller.html#setConverters(com.thoughtworks.xstream.converters.ConverterMatcher...)[custom converters] to make sure that only your supported classes can be unmarshalled. You might want to add a `CatchAllConverter` as the last converter in the list, in addition to converters that explicitly support the domain classes that should be supported. As a diff --git a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc index a4f7814a61f3..c27fd7ec4519 100644 --- a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc @@ -68,14 +68,14 @@ The simplest way to create a `DatabaseClient` object is through a static factory ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- DatabaseClient client = DatabaseClient.create(connectionFactory); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = DatabaseClient.create(connectionFactory) ---- @@ -95,7 +95,7 @@ parameter to database bind marker translation. run. * `….namedParameters(false)`: Disable named parameter expansion. Enabled by default. -TIP: Dialects are resolved by {api-spring-framework}/r2dbc/core/binding/BindMarkersFactoryResolver.html[`BindMarkersFactoryResolver`] +TIP: Dialects are resolved by {spring-framework-api}/r2dbc/core/binding/BindMarkersFactoryResolver.html[`BindMarkersFactoryResolver`] from a `ConnectionFactory`, typically by inspecting `ConnectionFactoryMetadata`. + You can let Spring auto-discover your `BindMarkersFactory` by registering a @@ -120,7 +120,7 @@ the reactive sequence to aid debugging. The following sections provide some examples of `DatabaseClient` usage. These examples are not an exhaustive list of all of the functionality exposed by the `DatabaseClient`. -See the attendant {api-spring-framework}/r2dbc/core/DatabaseClient.html[javadoc] for that. +See the attendant {spring-framework-api}/r2dbc/core/DatabaseClient.html[javadoc] for that. [[r2dbc-DatabaseClient-examples-statement]] ==== Executing Statements @@ -133,7 +133,7 @@ code that creates a new table: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[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(); @@ -141,7 +141,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") .await() @@ -170,7 +170,7 @@ The following query gets the `id` and `name` columns from a table: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono> first = client.sql("SELECT id, name FROM person") .fetch().first(); @@ -178,7 +178,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val first = client.sql("SELECT id, name FROM person") .fetch().awaitSingle() @@ -191,7 +191,7 @@ The following query uses a bind variable: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") .bind("fn", "Joe") @@ -200,7 +200,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") .bind("fn", "Joe") @@ -237,7 +237,7 @@ The following example extracts the `name` column and emits its value: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Flux names = client.sql("SELECT name FROM person") .map(row -> row.get("name", String.class)) @@ -246,7 +246,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val names = client.sql("SELECT name FROM person") .map{ row: Row -> row.get("name", String.class) } @@ -298,7 +298,7 @@ of updated rows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono affectedRows = client.sql("UPDATE person SET first_name = :fn") .bind("fn", "Joe") @@ -307,7 +307,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val affectedRows = client.sql("UPDATE person SET first_name = :fn") .bind("fn", "Joe") @@ -364,6 +364,28 @@ Or you may pass in a parameter object with bean properties or record components: .bindProperties(new Person("joe", "Joe", 34); ---- +Alternatively, you can use positional parameters for binding values to statements. +Indices are zero based. + +[source,java] +---- + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind(0, "joe") + .bind(1, "Joe") + .bind(2, 34); +---- + +In case your application is binding to many parameters, the same can be achieved with a single call: + +[source,java] +---- + List values = List.of("joe", "Joe", 34); + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bindValues(values); +---- + + + .R2DBC Native Bind Markers **** R2DBC uses database-native bind markers that depend on the actual database vendor. @@ -399,7 +421,7 @@ The preceding query can be parameterized and run as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- List tuples = new ArrayList<>(); tuples.add(new Object[] {"John", 35}); @@ -411,7 +433,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val tuples: MutableList> = ArrayList() tuples.add(arrayOf("John", 35)) @@ -430,7 +452,7 @@ The following example shows a simpler variant using `IN` predicates: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[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)); @@ -438,23 +460,19 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val tuples: MutableList> = ArrayList() - tuples.add(arrayOf("John", 35)) - tuples.add(arrayOf("Ann", 50)) - client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") - .bind("tuples", arrayOf(35, 50)) + .bind("ages", arrayOf(35, 50)) ---- ====== NOTE: R2DBC itself does not support Collection-like values. Nevertheless, expanding a given `List` in the example above works for named parameters -in Spring's R2DBC support, e.g. for use in `IN` clauses as shown above. -However, inserting or updating array-typed columns (e.g. in Postgres) +in Spring's R2DBC support, for example, for use in `IN` clauses as shown above. +However, inserting or updating array-typed columns (for example, in Postgres) requires an array type that is supported by the underlying R2DBC driver: -typically a Java array, e.g. `String[]` to update a `text[]` column. +typically a Java array, for example, `String[]` to update a `text[]` column. Do not pass `Collection` or the like as an array parameter. [[r2dbc-DatabaseClient-filter]] @@ -469,7 +487,7 @@ modify statements in their execution, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[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"))) @@ -479,7 +497,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter { s: Statement, next: ExecuteFunction -> next.execute(s.returnGeneratedValues("id")) } @@ -495,7 +513,7 @@ a `Function`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter(statement -> s.returnGeneratedValues("id")); @@ -506,7 +524,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter { statement -> s.returnGeneratedValues("id") } @@ -538,7 +556,7 @@ the setter for the `ConnectionFactory`. This leads to DAOs that resemble the fol ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class R2dbcCorporateEventDao implements CorporateEventDao { @@ -554,7 +572,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { @@ -576,7 +594,7 @@ method with `@Autowired`. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component // <1> public class R2dbcCorporateEventDao implements CorporateEventDao { @@ -597,7 +615,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component // <1> class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { // <2> @@ -633,7 +651,7 @@ requests the generated key for the desired column. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter(statement -> s.returnGeneratedValues("id")) @@ -645,7 +663,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter { statement -> s.returnGeneratedValues("id") } @@ -698,14 +716,14 @@ The following example shows how to configure a `ConnectionFactory`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); ---- @@ -752,7 +770,7 @@ the same time, have this client participating in Spring managed transactions. It preferable to integrate a R2DBC client with proper access to `ConnectionFactoryUtils` for resource management. -See the {api-spring-framework}/r2dbc/connection/TransactionAwareConnectionFactoryProxy.html[`TransactionAwareConnectionFactoryProxy`] +See the {spring-framework-api}/r2dbc/connection/TransactionAwareConnectionFactoryProxy.html[`TransactionAwareConnectionFactoryProxy`] javadoc for more details. diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/application-server-integration.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/application-server-integration.adoc index eb94d9516959..4e865292cdd4 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/application-server-integration.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/application-server-integration.adoc @@ -7,7 +7,7 @@ the JTA `UserTransaction` and `TransactionManager` objects) autodetects the loca the latter object, which varies by application server. Having access to the JTA `TransactionManager` allows for enhanced transaction semantics -- in particular, supporting transaction suspension. See the -{api-spring-framework}/transaction/jta/JtaTransactionManager.html[`JtaTransactionManager`] +{spring-framework-api}/transaction/jta/JtaTransactionManager.html[`JtaTransactionManager`] javadoc for details. Spring's `JtaTransactionManager` is the standard choice to run on Jakarta EE application diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc index 2ce54174913c..f6e75c09c35e 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc @@ -19,7 +19,7 @@ Consider the following class definition: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // the service class that we want to make transactional @Transactional @@ -49,7 +49,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // the service class that we want to make transactional @Transactional @@ -74,10 +74,11 @@ Kotlin:: ---- ====== -Used at the class level as above, the annotation indicates a default for all methods of -the declaring class (as well as its subclasses). Alternatively, each method can be -annotated individually. See xref:data-access/transaction/declarative/annotations.adoc#transaction-declarative-annotations-method-visibility[method visibility] for -further details on which methods Spring considers transactional. Note that a class-level +Used at the class level as above, the annotation indicates a default for all methods +of the declaring class (as well as its subclasses). Alternatively, each method can be +annotated individually. See +xref:data-access/transaction/declarative/annotations.adoc#transaction-declarative-annotations-method-visibility[method visibility] +for further details on which methods Spring considers transactional. Note that a class-level annotation does not apply to ancestor classes up the class hierarchy; in such a scenario, inherited methods need to be locally redeclared in order to participate in a subclass-level annotation. @@ -85,7 +86,7 @@ subclass-level annotation. When a POJO class such as the one above is defined as a bean in a Spring context, you can make the bean instance transactional through an `@EnableTransactionManagement` annotation in a `@Configuration` class. See the -{api-spring-framework}/transaction/annotation/EnableTransactionManagement.html[javadoc] +{spring-framework-api}/transaction/annotation/EnableTransactionManagement.html[javadoc] for full details. In XML configuration, the `` tag provides similar convenience: @@ -137,7 +138,7 @@ programming arrangements as the following listing shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // the reactive service class that we want to make transactional @Transactional @@ -167,7 +168,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // the reactive service class that we want to make transactional @Transactional @@ -249,7 +250,7 @@ the proxy are intercepted. This means that self-invocation (in effect, a method the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with `@Transactional`. Also, the proxy must be fully initialized to provide the expected behavior, so you should not -rely on this feature in your initialization code -- e.g. in a `@PostConstruct` method. +rely on this feature in your initialization code -- for example, in a `@PostConstruct` method. Consider using AspectJ mode (see the `mode` attribute in the following table) if you expect self-invocations to be wrapped with transactions as well. In this case, there is @@ -262,7 +263,7 @@ is modified) to support `@Transactional` runtime behavior on any kind of method. | XML Attribute| Annotation Attribute| Default| Description | `transaction-manager` -| N/A (see {api-spring-framework}/transaction/annotation/TransactionManagementConfigurer.html[`TransactionManagementConfigurer`] javadoc) +| N/A (see {spring-framework-api}/transaction/annotation/TransactionManagementConfigurer.html[`TransactionManagementConfigurer`] javadoc) | `transactionManager` | Name of the transaction manager to use. Required only if the name of the transaction manager is not `transactionManager`, as in the preceding example. @@ -326,7 +327,7 @@ precedence over the transactional settings defined at the class level. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Transactional(readOnly = true) public class DefaultFooService implements FooService { @@ -345,7 +346,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Transactional(readOnly = true) class DefaultFooService : FooService { @@ -436,16 +437,37 @@ properties of the `@Transactional` annotation: | Optional array of exception name patterns that must not cause rollback. |=== -TIP: See xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules] for further details -on rollback rule semantics, patterns, and warnings regarding possible unintentional -matches for pattern-based rollback rules. +TIP: See +xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules] +for further details on rollback rule semantics, patterns, and warnings +regarding possible unintentional matches for pattern-based rollback rules. + +[NOTE] +==== +As of 6.2, you can globally change the default rollback behavior – for example, through +`@EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS)`, leading to a rollback +for all exceptions raised within a transaction, including any checked exception. +For further customizations, `AnnotationTransactionAttributeSource` provides an +`addDefaultRollbackRule(RollbackRuleAttribute)` method for custom default rules. + +Note that transaction-specific rollback rules override the default behavior but +retain the chosen default for unspecified exceptions. This is the case for +Spring's `@Transactional` as well as JTA's `jakarta.transaction.Transactional` +annotation. + +Unless you rely on EJB-style business exceptions with commit behavior, it is +advisable to switch to `ALL_EXCEPTIONS` for consistent rollback semantics even +in case of a (potentially accidental) checked exception. Also, it is advisable +to make that switch for Kotlin-based applications where there is no enforcement +of checked exceptions at all. +==== Currently, you cannot have explicit control over the name of a transaction, where 'name' means the transaction name that appears in a transaction monitor and in logging output. For declarative transactions, the transaction name is always the fully-qualified class -name + `.` + the method name of the transactionally advised class. For example, if the +name of the transactionally advised class + `.` + the method name. For example, if the `handlePayment(..)` method of the `BusinessService` class started a transaction, the -name of the transaction would be: `com.example.BusinessService.handlePayment`. +name of the transaction would be `com.example.BusinessService.handlePayment`. [[tx-multiple-tx-mgrs-with-attransactional]] == Multiple Transaction Managers with `@Transactional` @@ -463,7 +485,7 @@ in the application context: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class TransactionalService { @@ -480,7 +502,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- class TransactionalService { @@ -529,19 +551,39 @@ transaction managers, differentiated by the `order`, `account`, and `reactive-ac qualifiers. The default `` target bean name, `transactionManager`, is still used if no specifically qualified `TransactionManager` bean is found. +[TIP] +==== +If all transactional methods on the same class share the same qualifier, consider +declaring a type-level `org.springframework.beans.factory.annotation.Qualifier` +annotation instead. If its value matches the qualifier value (or bean name) of a +specific transaction manager, that transaction manager is going to be used for +transaction definitions without a specific qualifier on `@Transactional` itself. + +Such a type-level qualifier can be declared on the concrete class, applying to +transaction definitions from a base class as well. This effectively overrides +the default transaction manager choice for any unqualified base class methods. + +Last but not least, such a type-level bean qualifier can serve multiple purposes, +for example, with a value of "order" it can be used for autowiring purposes (identifying +the order repository) as well as transaction manager selection, as long as the +target beans for autowiring as well as the associated transaction manager +definitions declare the same qualifier value. Such a qualifier value only needs +to be unique within a set of type-matching beans, not having to serve as an ID. +==== + [[tx-custom-attributes]] == Custom Composed Annotations -If you find you repeatedly use the same attributes with `@Transactional` on many different -methods, xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support] lets you -define custom composed annotations for your specific use cases. For example, consider the +If you find you repeatedly use the same attributes with `@Transactional` on many different methods, +xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support] +lets you define custom composed annotations for your specific use cases. For example, consider the following annotation definitions: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @@ -558,7 +600,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @@ -578,7 +620,7 @@ The preceding annotations let us write the example from the previous section as ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class TransactionalService { @@ -596,7 +638,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- class TransactionalService { diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/applying-more-than-just-tx-advice.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/applying-more-than-just-tx-advice.adoc index bcd41b9ab7cf..1a14bb5e499e 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/applying-more-than-just-tx-advice.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/applying-more-than-just-tx-advice.adoc @@ -22,7 +22,7 @@ The following code shows the simple profiling aspect discussed earlier: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package x.y; @@ -61,7 +61,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package x.y diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/aspectj.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/aspectj.adoc index 58a65cafff62..b112eef49519 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/aspectj.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/aspectj.adoc @@ -24,7 +24,7 @@ The following example shows how to create a transaction manager and configure th ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // construct an appropriate transaction manager DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource()); @@ -35,7 +35,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // construct an appropriate transaction manager val txManager = DataSourceTransactionManager(getDataSource()) @@ -57,8 +57,7 @@ transaction semantics given by the class annotation (if present). You can annota regardless of visibility. To weave your applications with the `AnnotationTransactionAspect`, you must either build -your application with AspectJ (see the -https://www.eclipse.org/aspectj/doc/released/devguide/index.html[AspectJ Development +your application with AspectJ (see the {aspectj-docs-devguide}/index.html[AspectJ Development Guide]) or use load-time weaving. See xref:core/aop/using-aspectj.adoc#aop-aj-ltw[Load-time weaving with AspectJ in the Spring Framework] for a discussion of load-time weaving with AspectJ. diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/first-example.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/first-example.adoc index c84bd17412ff..692de56d9e1f 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/first-example.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/first-example.adoc @@ -14,7 +14,7 @@ interface: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- // the service interface that we want to make transactional @@ -35,7 +35,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- // the service interface that we want to make transactional @@ -60,7 +60,7 @@ The following example shows an implementation of the preceding interface: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package x.y.service; @@ -90,7 +90,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package x.y.service @@ -231,7 +231,7 @@ that test drives the configuration shown earlier: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public final class Boot { @@ -245,7 +245,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -303,7 +303,7 @@ this time the code uses reactive types: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- // the reactive service interface that we want to make transactional @@ -324,7 +324,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- // the reactive service interface that we want to make transactional @@ -349,7 +349,7 @@ The following example shows an implementation of the preceding interface: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package x.y.service; @@ -379,7 +379,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package x.y.service diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc index b002be989d24..c261821fa235 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc @@ -19,18 +19,18 @@ marks a transaction for rollback only in the case of runtime, unchecked exceptio That is, when the thrown exception is an instance or subclass of `RuntimeException`. (`Error` instances also, by default, result in a rollback). -As of Spring Framework 5.2, the default configuration also provides support for -Vavr's `Try` method to trigger transaction rollbacks when it returns a 'Failure'. +The default configuration also provides support for Vavr's `Try` method to trigger +transaction rollbacks when it returns a 'Failure'. This allows you to handle functional-style errors using Try and have the transaction automatically rolled back in case of a failure. For more information on Vavr's Try, -refer to the [official Vavr documentation](https://www.vavr.io/vavr-docs/#_try). - +refer to the {vavr-docs}/#_try[official Vavr documentation]. Here's an example of how to use Vavr's Try with a transactional method: + [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Transactional public Try myTransactionalMethod() { @@ -42,6 +42,32 @@ Java:: ---- ====== +As of Spring Framework 6.1, there is also special treatment of `CompletableFuture` +(and general `Future`) return values, triggering a rollback for such a handle if it +was exceptionally completed at the time of being returned from the original method. +This is intended for `@Async` methods where the actual method implementation may +need to comply with a `CompletableFuture` signature (auto-adapted to an actual +asynchronous handle for a call to the proxy by `@Async` processing at runtime), +preferring exposure in the returned handle rather than rethrowing an exception: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Transactional @Async + public CompletableFuture myTransactionalMethod() { + try { + return CompletableFuture.completedFuture(delegate.myDataAccessOperation()); + } + catch (DataAccessException ex) { + return CompletableFuture.failedFuture(ex); + } + } +---- +====== + Checked exceptions that are thrown from a transactional method do not result in a rollback in the default configuration. You can configure exactly which `Exception` types mark a transaction for rollback, including checked exceptions by specifying _rollback rules_. @@ -146,7 +172,7 @@ rollback: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public void resolvePosition() { try { @@ -160,7 +186,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun resolvePosition() { try { diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-decl-explained.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-decl-explained.adoc index f59bb26b0005..afece4181a30 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-decl-explained.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-decl-explained.adoc @@ -38,6 +38,11 @@ within the method. A reactive transaction managed by `ReactiveTransactionManager` uses the Reactor context instead of thread-local attributes. As a consequence, all participating data access operations need to execute within the same Reactor context in the same reactive pipeline. + +When configured with a `ReactiveTransactionManager`, all transaction-demarcated methods +are expected to return a reactive pipeline. Void methods or regular return types need +to be associated with a regular `PlatformTransactionManager`, for example, through the +`transactionManager` attribute of the corresponding `@Transactional` declarations. ==== The following image shows a conceptual view of calling a method on a transactional proxy: diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-propagation.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-propagation.adoc index cf4bceac371b..b41fd48f0d51 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-propagation.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/tx-propagation.adoc @@ -75,6 +75,6 @@ that it can roll back to. Such partial rollbacks let an inner transaction scope trigger a rollback for its scope, with the outer transaction being able to continue the physical transaction despite some operations having been rolled back. This setting is typically mapped onto JDBC savepoints, so it works only with JDBC resource -transactions. See Spring's {api-spring-framework}/jdbc/datasource/DataSourceTransactionManager.html[`DataSourceTransactionManager`]. +transactions. See Spring's {spring-framework-api}/jdbc/datasource/DataSourceTransactionManager.html[`DataSourceTransactionManager`]. diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc index 8d34460168fe..8ee00835b25f 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc @@ -19,7 +19,7 @@ example sets up such an event listener: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component public class MyComponent { @@ -33,7 +33,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class MyComponent { @@ -63,7 +63,7 @@ For the former, listeners are guaranteed to see the current thread-bound transac Since the latter uses the Reactor context instead of thread-local variables, the transaction context needs to be included in the published event instance as the event source. See the -{api-spring-framework}/transaction/reactive/TransactionalEventPublisher.html[`TransactionalEventPublisher`] +{spring-framework-api}/transaction/reactive/TransactionalEventPublisher.html[`TransactionalEventPublisher`] javadoc for details. ==== diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/programmatic.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/programmatic.adoc index 6c4bbb7021fa..1c14f7893a54 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/programmatic.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/programmatic.adoc @@ -37,7 +37,7 @@ a transaction. You can then pass an instance of your custom `TransactionCallback ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleService implements Service { @@ -63,7 +63,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // use constructor-injection to supply the PlatformTransactionManager class SimpleService(transactionManager: PlatformTransactionManager) : Service { @@ -87,7 +87,7 @@ with an anonymous class, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { @@ -99,7 +99,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- transactionTemplate.execute(object : TransactionCallbackWithoutResult() { override fun doInTransactionWithoutResult(status: TransactionStatus) { @@ -118,7 +118,7 @@ Code within the callback can roll the transaction back by calling the ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- transactionTemplate.execute(new TransactionCallbackWithoutResult() { @@ -135,7 +135,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- transactionTemplate.execute(object : TransactionCallbackWithoutResult() { @@ -165,7 +165,7 @@ a specific `TransactionTemplate:` ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleService implements Service { @@ -184,7 +184,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleService(transactionManager: PlatformTransactionManager) : Service { @@ -240,7 +240,7 @@ the `TransactionalOperator` resembles the next example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleService implements Service { @@ -265,7 +265,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // use constructor-injection to supply the ReactiveTransactionManager class SimpleService(transactionManager: ReactiveTransactionManager) : Service { @@ -293,7 +293,7 @@ method on the supplied `ReactiveTransaction` object, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- transactionalOperator.execute(new TransactionCallback<>() { @@ -307,7 +307,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- transactionalOperator.execute(object : TransactionCallback() { @@ -346,7 +346,7 @@ following example shows customization of the transactional settings for a specif ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleService implements Service { @@ -367,7 +367,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleService(transactionManager: ReactiveTransactionManager) : Service { @@ -402,7 +402,7 @@ following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- DefaultTransactionDefinition def = new DefaultTransactionDefinition(); // explicitly setting the transaction name is something that can be done only programmatically @@ -421,7 +421,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val def = DefaultTransactionDefinition() // explicitly setting the transaction name is something that can be done only programmatically @@ -455,7 +455,7 @@ following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- DefaultTransactionDefinition def = new DefaultTransactionDefinition(); // explicitly setting the transaction name is something that can be done only programmatically @@ -475,7 +475,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val def = DefaultTransactionDefinition() // explicitly setting the transaction name is something that can be done only programmatically diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/strategies.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/strategies.adoc index d64cef5a2bf3..d052af9dd6ea 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/strategies.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/strategies.adoc @@ -45,9 +45,9 @@ exists in the current call stack. The implication in this latter case is that, a Jakarta EE transaction contexts, a `TransactionStatus` is associated with a thread of execution. -As of Spring Framework 5.2, Spring also provides a transaction management abstraction for -reactive applications that make use of reactive types or Kotlin Coroutines. The following -listing shows the transaction strategy defined by +Spring also provides a transaction management abstraction for reactive applications that +make use of reactive types or Kotlin Coroutines. The following listing shows the +transaction strategy defined by `org.springframework.transaction.ReactiveTransactionManager`: [source,java,indent=0,subs="verbatim,quotes"] diff --git a/framework-docs/modules/ROOT/pages/index.adoc b/framework-docs/modules/ROOT/pages/index.adoc index a99335c230ea..d3157a5c6a9a 100644 --- a/framework-docs/modules/ROOT/pages/index.adoc +++ b/framework-docs/modules/ROOT/pages/index.adoc @@ -7,7 +7,7 @@ xref:overview.adoc[Overview] :: History, Design Philosophy, Feedback, Getting Started. xref:core.adoc[Core] :: IoC Container, Events, Resources, i18n, Validation, Data Binding, Type Conversion, SpEL, AOP, AOT. -<> :: Mock Objects, TestContext Framework, +xref:testing.adoc[Testing] :: Mock Objects, TestContext Framework, Spring MVC Test, WebTestClient. xref:data-access.adoc[Data Access] :: Transactions, DAO Support, JDBC, R2DBC, O/R Mapping, XML Marshalling. @@ -18,8 +18,8 @@ WebSocket, RSocket. xref:integration.adoc[Integration] :: REST Clients, JMS, JCA, JMX, Email, Tasks, Scheduling, Caching, Observability, JVM Checkpoint Restore. xref:languages.adoc[Languages] :: Kotlin, Groovy, Dynamic Languages. -xref:testing/appendix.adoc[Appendix] :: Spring properties. -https://github.com/spring-projects/spring-framework/wiki[Wiki] :: What's New, +xref:appendix.adoc[Appendix] :: Spring properties. +{spring-framework-wiki}[Wiki] :: What's New, Upgrade Notes, Supported Versions, additional cross-version information. Rod Johnson, Juergen Hoeller, Keith Donald, Colin Sampaleanu, Rob Harrop, Thomas Risberg, @@ -29,8 +29,6 @@ Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clem Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch -Copyright © 2002 - 2023 VMware, Inc. All Rights Reserved. - Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each -copy contains this Copyright Notice, whether distributed in print or electronically. +copy contains the Copyright Notice, whether distributed in print or electronically. 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/cache/annotations.adoc b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc index aa1d408df829..61ecce6957ed 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc @@ -67,7 +67,7 @@ To provide a different default key generator, you need to implement the The default key generation strategy changed with the release of Spring 4.0. Earlier versions of Spring used a key generation strategy that, for multiple key parameters, considered only the `hashCode()` of parameters and not `equals()`. This could cause -unexpected key collisions (see https://jira.spring.io/browse/SPR-10237[SPR-10237] +unexpected key collisions (see {spring-framework-issues}/14870[spring-framework#14870] for background). The new `SimpleKeyGenerator` uses a compound key for such scenarios. If you want to keep using the previous key strategy, you can configure the deprecated @@ -332,7 +332,7 @@ metadata, such as the argument names. The following table describes the items ma available to the context so that you can use them for key and conditional computations: [[cache-spel-context-tbl]] -.Cache SpEL available metadata +.Cache metadata available in SpEL expressions |=== | Name| Location| Description| Example @@ -358,7 +358,7 @@ available to the context so that you can use them for key and conditional comput | `args` | Root object -| The arguments (as array) used for invoking the target +| The arguments (as an object array) used for invoking the target | `#root.args[0]` | `caches` @@ -368,9 +368,10 @@ available to the context so that you can use them for key and conditional comput | Argument name | Evaluation context -| Name of any of the method arguments. If the names are not available - (perhaps due to having no debug information), the argument names are also available under the `#a<#arg>` - where `#arg` stands for the argument index (starting from `0`). +| The name of a particular method argument. If the names are not available + (for example, because the code was compiled without the `-parameters` flag), individual + arguments are also available using the `#a<#arg>` syntax where `<#arg>` stands for the + argument index (starting from 0). | `#iban` or `#a0` (you can also use `#p0` or `#p<#arg>` notation as an alias). | `result` @@ -500,12 +501,12 @@ Placing this annotation on the class does not turn on any caching operation. An operation-level customization always overrides a customization set on `@CacheConfig`. Therefore, this gives three levels of customizations for each cache operation: -* Globally configured, e.g. through `CachingConfigurer`: see next section. +* Globally configured, for example, through `CachingConfigurer`: see next section. * At the class level, using `@CacheConfig`. * At the operation level. NOTE: Provider-specific settings are typically available on the `CacheManager` bean, -e.g. on `CaffeineCacheManager`. These are effectively also global. +for example, on `CaffeineCacheManager`. These are effectively also global. [[cache-annotation-enable]] @@ -518,41 +519,9 @@ disable it by removing only one configuration line rather than all the annotatio your code). To enable caching annotations add the annotation `@EnableCaching` to one of your -`@Configuration` classes: +`@Configuration` classes or use the `cache:annotation-driven` element with XML: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableCaching - public class AppConfig { - - @Bean - CacheManager cacheManager() { - CaffeineCacheManager cacheManager = new CaffeineCacheManager(); - cacheManager.setCacheSpecification(...); - return cacheManager; - } - } ----- - -Alternatively, for XML configuration you can use the `cache:annotation-driven` element: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- +include-code::./CacheConfiguration[tag=snippet,indent=0] Both the `cache:annotation-driven` element and the `@EnableCaching` annotation let you specify various options that influence the way the caching behavior is added to the @@ -566,7 +535,7 @@ switching to `aspectj` mode in combination with compile-time or load-time weavin NOTE: For more detail about advanced customizations (using Java configuration) that are required to implement `CachingConfigurer`, see the -{api-spring-framework}/cache/annotation/CachingConfigurer.html[javadoc]. +{spring-framework-api}/cache/annotation/CachingConfigurer.html[javadoc]. [[cache-annotation-driven-settings]] .Cache annotation settings @@ -575,7 +544,7 @@ required to implement `CachingConfigurer`, see the | XML Attribute | Annotation Attribute | Default | Description | `cache-manager` -| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| N/A (see the {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) | `cacheManager` | The name of the cache manager to use. A default `CacheResolver` is initialized behind the scenes with this cache manager (or `cacheManager` if not set). For more @@ -583,19 +552,19 @@ required to implement `CachingConfigurer`, see the attribute. | `cache-resolver` -| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| N/A (see the {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) | A `SimpleCacheResolver` using the configured `cacheManager`. | The bean name of the CacheResolver that is to be used to resolve the backing caches. This attribute is not required and needs to be specified only as an alternative to the 'cache-manager' attribute. | `key-generator` -| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| N/A (see the {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) | `SimpleKeyGenerator` | Name of the custom key generator to use. | `error-handler` -| N/A (see the {api-spring-framework}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) +| N/A (see the {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`] javadoc) | `SimpleCacheErrorHandler` | The name of the custom cache error handler to use. By default, any exception thrown during a cache related operation is thrown back at the client. diff --git a/framework-docs/modules/ROOT/pages/integration/cache/store-configuration.adoc b/framework-docs/modules/ROOT/pages/integration/cache/store-configuration.adoc index c8009ce25154..ed350b2385e5 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/store-configuration.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/store-configuration.adoc @@ -13,18 +13,7 @@ The JDK-based `Cache` implementation resides under `org.springframework.cache.concurrent` package. It lets you use `ConcurrentHashMap` as a backing `Cache` store. The following example shows how to configure two caches: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - ----- +include-code::./CacheConfiguration[tag=snippet,indent=0] The preceding snippet uses the `SimpleCacheManager` to create a `CacheManager` for the two nested `ConcurrentMapCache` instances named `default` and `books`. Note that the @@ -52,26 +41,12 @@ of Caffeine. The following example configures a `CacheManager` that creates the cache on demand: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +include-code::./CacheConfiguration[tag=snippet,indent=0] You can also provide the caches to use explicitly. In that case, only those are made available by the manager. The following example shows how to do so: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - default - books - - - ----- +include-code::./CustomCacheConfiguration[tag=snippet,indent=0] The Caffeine `CacheManager` also supports custom `Caffeine` and `CacheLoader`. See the https://github.com/ben-manes/caffeine/wiki[Caffeine documentation] @@ -97,15 +72,7 @@ implementation is located in the `org.springframework.cache.jcache` package. Again, to use it, you need to declare the appropriate `CacheManager`. The following example shows how to do so: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - ----- +include-code::./CacheConfiguration[tag=snippet,indent=0] [[cache-store-configuration-noop]] @@ -119,18 +86,7 @@ cache declarations (which can prove tedious), you can wire in a simple dummy cac performs no caching -- that is, it forces the cached methods to be invoked every time. The following example shows how to do so: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - ----- +include-code::./CacheConfiguration[tag=snippet,indent=0] The `CompositeCacheManager` in the preceding chains multiple `CacheManager` instances and, through the `fallbackToNoOpCache` flag, adds a no-op cache for all the definitions not diff --git a/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc b/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc index 184617df0106..934b95b1dc85 100644 --- a/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc +++ b/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc @@ -1,31 +1,40 @@ [[checkpoint-restore]] = JVM Checkpoint Restore -The Spring Framework integrates with checkpoint/restore as implemented by https://github.com/CRaC/docs[Project CRaC] in order to allow implementing systems capable to reduce the startup and warmup times of Spring-based Java applications with the JVM. +The Spring Framework integrates with checkpoint/restore as implemented by https://github.com/CRaC/docs[Project CRaC] in order to allow implementing systems capable of reducing the startup and warmup times of Spring-based Java applications with the JVM. Using this feature requires: * A checkpoint/restore enabled JVM (Linux only for now). -* The presence in the classpath of the https://github.com/CRaC/org.crac[`org.crac:crac`] library (version `1.4.0` and above are supported). -* Specifying the required `java` command line parameters like `-XX:CRaCCheckpointTo=PATH` or `-XX:CRaCRestoreFrom=PATH`. +* The presence of the https://github.com/CRaC/org.crac[`org.crac:crac`] library (version `1.4.0` and above are supported) in the classpath. +* Specifying the required `java` command-line parameters like `-XX:CRaCCheckpointTo=PATH` or `-XX:CRaCRestoreFrom=PATH`. -WARNING: The files generated in the path specified by `-XX:CRaCCheckpointTo=PATH` when a checkpoint is requested contain a representation of the memory of the running JVM, which may contain secrets and other sensitive data. Using this feature should be done with the assumption that any value "seen" by the JVM, such as configuration properties coming from the environment, will be stored in those CRaC files. As a consequence, the security implications of where and how those files are generated, stored and accessed should be carefully assessed. +WARNING: The files generated in the path specified by `-XX:CRaCCheckpointTo=PATH` when a checkpoint is requested contain a representation of the memory of the running JVM, which may contain secrets and other sensitive data. Using this feature should be done with the assumption that any value "seen" by the JVM, such as configuration properties coming from the environment, will be stored in those CRaC files. As a consequence, the security implications of where and how those files are generated, stored, and accessed should be carefully assessed. -Conceptually, checkpoint and restore match with xref:core/beans/factory-nature.adoc#beans-factory-lifecycle-processor[Spring `Lifecycle` contract] for individual beans. +Conceptually, checkpoint and restore align with the xref:core/beans/factory-nature.adoc#beans-factory-lifecycle-processor[Spring `Lifecycle` contract] for individual beans. -== On demand checkpoint/restore of a running application +== On-demand checkpoint/restore of a running application -A checkpoint can be created on demand, for example using a command like `jcmd application.jar JDK.checkpoint`. Before the creation of the checkpoint, Spring Framework -stops all the running beans, giving them a chance to close resources if needed by implementing `Lifecycle.stop`. After restore, the same beans are restarted, with `Lifecycle.start` allowing to reopen resources when relevant. For libraries not depending on Spring, checkpoint/restore custom integration can be provided by implementing `org.crac.Resource` and registering the related instance. +A checkpoint can be created on demand, for example using a command like `jcmd application.jar JDK.checkpoint`. Before the creation of the checkpoint, Spring stops all the running beans, giving them a chance to close resources if needed by implementing `Lifecycle.stop`. After restore, the same beans are restarted, with `Lifecycle.start` allowing beans to reopen resources when relevant. For libraries that do not depend on Spring, custom checkpoint/restore integration can be provided by implementing `org.crac.Resource` and registering the related instance. WARNING: Leveraging checkpoint/restore of a running application typically requires additional lifecycle management to gracefully stop and start using resources like files or sockets and stop active threads. +WARNING: Be aware that when defining scheduling tasks at a fixed rate, for example with an annotation like `@Scheduled(fixedRate = 5000)`, all missed executions between checkpoint and restore will be performed when the JVM is restored with on-demand checkpoint/restore. If this is not the behavior you want, it is recommended to schedule tasks at a fixed delay (for example with `@Scheduled(fixedDelay = 5000)`) or with a cron expression as those are calculated after every task execution. + NOTE: If the checkpoint is created on a warmed-up JVM, the restored JVM will be equally warmed-up, allowing potentially peak performance immediately. This method typically requires access to remote services, and thus requires some level of platform integration. == Automatic checkpoint/restore at startup -When the `-Dspring.context.checkpoint=onRefresh` Java system property is set, a checkpoint is created automatically during the startup at `LifecycleProcessor.onRefresh` level. At this phase, all non-lazy initialized singletons are instantiated, `InitializingBean.afterPropertiesSet` callbacks have been invoked, but not `Lifecycle.start` ones and `ContextRefreshedEvent` has not yet been published. +When the `-Dspring.context.checkpoint=onRefresh` JVM system property is set, a checkpoint is created automatically at +startup during the `LifecycleProcessor.onRefresh` phase. After this phase has completed, 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. + +For testing purposes, it is also possible to leverage the `-Dspring.context.exit=onRefresh` JVM system property which +triggers similar behavior, but instead of creating a checkpoint, it exits your Spring application at the same lifecycle +phase without requiring the Project CraC dependency/JVM or Linux. This can be useful to check if connections to remote +services are required when the beans are not started, and potentially refine the configuration to avoid that. WARNING: As mentioned above, and especially in use cases where the CRaC files are shipped as part of a deployable artifact (a container image for example), operate with the assumption that any sensitive data "seen" by the JVM ends up in the CRaC files, and assess carefully the related security implications. -NOTE: Here checkpoint/restore is a way to "fast-forward" the startup of the application to a phase where the application context is about to start, but does not allow to have a fully warmed-up JVM. +NOTE: Automatic checkpoint/restore is a way to "fast-forward" the startup of the application to a phase where the application context is about to start, but it does not allow to have a fully warmed-up JVM. diff --git a/framework-docs/modules/ROOT/pages/integration/email.adoc b/framework-docs/modules/ROOT/pages/integration/email.adoc index 610fda7c8797..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 @@ -26,7 +24,7 @@ interface. A simple value object that encapsulates the properties of a simple ma as `from` and `to` (plus many others) is the `SimpleMailMessage` class. This package also contains a hierarchy of checked exceptions that provide a higher level of abstraction over the lower level mail system exceptions, with the root exception being -`MailException`. See the {api-spring-framework}/mail/MailException.html[javadoc] +`MailException`. See the {spring-framework-api}/mail/MailException.html[javadoc] for more information on the rich mail exception hierarchy. The `org.springframework.mail.javamail.JavaMailSender` interface adds specialized @@ -41,14 +39,7 @@ JavaMail features, such as MIME message support to the `MailSender` interface Assume that we have a business interface called `OrderManager`, as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - public interface OrderManager { - - void placeOrder(Order order); - - } ----- +include-code::./OrderManager[tag=snippet,indent=0] Further assume that we have a requirement stating that an email message with an order number needs to be generated and sent to a customer who placed the relevant order. @@ -60,70 +51,11 @@ order number needs to be generated and sent to a customer who placed the relevan The following example shows how to use `MailSender` and `SimpleMailMessage` to send an email when someone places an order: -[source,java,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.mail.MailException; - import org.springframework.mail.MailSender; - import org.springframework.mail.SimpleMailMessage; - - public class SimpleOrderManager implements OrderManager { - - private MailSender mailSender; - private SimpleMailMessage templateMessage; - - public void setMailSender(MailSender mailSender) { - this.mailSender = mailSender; - } - - public void setTemplateMessage(SimpleMailMessage templateMessage) { - this.templateMessage = templateMessage; - } - - public void placeOrder(Order order) { - - // Do the business calculations... - - // Call the collaborators to persist the order... - - // Create a thread safe "copy" of the template message and customize it - SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage); - msg.setTo(order.getCustomer().getEmailAddress()); - msg.setText( - "Dear " + order.getCustomer().getFirstName() - + order.getCustomer().getLastName() - + ", thank you for placing order. Your order number is " - + order.getOrderNumber()); - try { - this.mailSender.send(msg); - } - catch (MailException ex) { - // simply log it and go on... - System.err.println(ex.getMessage()); - } - } - - } ----- +include-code::./SimpleOrderManager[tag=snippet,indent=0] The following example shows the bean definitions for the preceding code: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - ----- +include-code::./MailConfiguration[tag=snippet,indent=0] [[mail-usage-mime]] diff --git a/framework-docs/modules/ROOT/pages/integration/jms.adoc b/framework-docs/modules/ROOT/pages/integration/jms.adoc index ee207db0e088..d3ca79413b48 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms.adoc @@ -6,7 +6,7 @@ the same way as Spring's integration does for the JDBC API. JMS can be roughly divided into two areas of functionality, namely the production and consumption of messages. The `JmsTemplate` class is used for message production and -synchronous message reception. For asynchronous reception similar to Jakarta EE's +synchronous message receipt. For asynchronous receipt similar to Jakarta EE's message-driven bean style, Spring provides a number of message-listener containers that you can use to create Message-Driven POJOs (MDPs). Spring also provides a declarative way to create message listeners. diff --git a/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc b/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc index edda9de3d387..586538b49199 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc @@ -37,52 +37,20 @@ declarations to it. To enable support for `@JmsListener` annotations, you can add `@EnableJms` to one of your `@Configuration` classes, as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableJms - public class AppConfig { - - @Bean - public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() { - DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory()); - factory.setDestinationResolver(destinationResolver()); - factory.setSessionTransacted(true); - factory.setConcurrency("3-10"); - return factory; - } - } ----- +include-code::./JmsConfiguration[tag=snippet,indent=0] By default, the infrastructure looks for a bean named `jmsListenerContainerFactory` as the source for the factory to use to create message listener containers. In this case (and ignoring the JMS infrastructure setup), you can invoke the `processOrder` -method with a core poll size of three threads and a maximum pool size of ten threads. +method with a core pool size of three threads and a maximum pool size of ten threads. You can customize the listener container factory to use for each annotation or you can configure an explicit default by implementing the `JmsListenerConfigurer` interface. The default is required only if at least one endpoint is registered without a specific container factory. See the javadoc of classes that implement -{api-spring-framework}/jms/annotation/JmsListenerConfigurer.html[`JmsListenerConfigurer`] +{spring-framework-api}/jms/annotation/JmsListenerConfigurer.html[`JmsListenerConfigurer`] for details and examples. -If you prefer xref:integration/jms/namespace.adoc[XML configuration], you can use the `` -element, as the following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- - [[jms-annotated-programmatic-registration]] == Programmatic Endpoint Registration diff --git a/framework-docs/modules/ROOT/pages/integration/jms/jca-message-endpoint-manager.adoc b/framework-docs/modules/ROOT/pages/integration/jms/jca-message-endpoint-manager.adoc index 23ce156526d3..8838f449d702 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/jca-message-endpoint-manager.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/jca-message-endpoint-manager.adoc @@ -7,66 +7,17 @@ automatically determine the `ActivationSpec` class name from the provider's `ResourceAdapter` class name. Therefore, it is typically possible to provide Spring's generic `JmsActivationSpecConfig`, as the following example shows: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - ----- +include-code::./JmsConfiguration[tag=snippet,indent=0] Alternatively, you can set up a `JmsMessageEndpointManager` with a given `ActivationSpec` object. The `ActivationSpec` object may also come from a JNDI lookup (using ``). The following example shows how to do so: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - ----- +include-code::./AlternativeJmsConfiguration[tag=snippet,indent=0] -Using Spring's `ResourceAdapterFactoryBean`, you can configure the target `ResourceAdapter` -locally, as the following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - ----- - -The specified `WorkManager` can also point to an environment-specific thread pool -- -typically through a `SimpleTaskWorkManager` instance's `asyncTaskExecutor` property. -Consider defining a shared thread pool for all your `ResourceAdapter` instances -if you happen to use multiple adapters. - -In some environments, you can instead obtain the entire `ResourceAdapter` object from JNDI -(by using ``). The Spring-based message listeners can then interact with -the server-hosted `ResourceAdapter`, which also use the server's built-in `WorkManager`. - -See the javadoc for {api-spring-framework}/jms/listener/endpoint/JmsMessageEndpointManager.html[`JmsMessageEndpointManager`], -{api-spring-framework}/jms/listener/endpoint/JmsActivationSpecConfig.html[`JmsActivationSpecConfig`], -and {api-spring-framework}/jca/support/ResourceAdapterFactoryBean.html[`ResourceAdapterFactoryBean`] +See the javadoc for {spring-framework-api}/jms/listener/endpoint/JmsMessageEndpointManager.html[`JmsMessageEndpointManager`], +{spring-framework-api}/jms/listener/endpoint/JmsActivationSpecConfig.html[`JmsActivationSpecConfig`], +and {spring-framework-api}/jca/support/ResourceAdapterFactoryBean.html[`ResourceAdapterFactoryBean`] for more details. Spring also provides a generic JCA message endpoint manager that is not tied to JMS: @@ -74,7 +25,7 @@ Spring also provides a generic JCA message endpoint manager that is not tied to for using any message listener type (such as a JMS `MessageListener`) and any provider-specific `ActivationSpec` object. See your JCA provider's documentation to find out about the actual capabilities of your connector, and see the -{api-spring-framework}/jca/endpoint/GenericMessageEndpointManager.html[`GenericMessageEndpointManager`] +{spring-framework-api}/jca/endpoint/GenericMessageEndpointManager.html[`GenericMessageEndpointManager`] javadoc for the Spring-specific configuration details. NOTE: JCA-based message endpoint management is very analogous to EJB 2.1 Message-Driven Beans. diff --git a/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc b/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc index be6cb7b45cbd..8ecda386a372 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc @@ -113,7 +113,7 @@ as the following example shows: ---- The following table describes all available attributes. See the class-level javadoc -of the {api-spring-framework}/jms/listener/AbstractMessageListenerContainer.html[`AbstractMessageListenerContainer`] +of the {spring-framework-api}/jms/listener/AbstractMessageListenerContainer.html[`AbstractMessageListenerContainer`] and its concrete subclasses for more details on the individual properties. The javadoc also provides a discussion of transaction choices and message redelivery scenarios. @@ -254,7 +254,7 @@ The following table describes the available configuration options for the JCA va | `activation-spec-factory` | A reference to the `JmsActivationSpecFactory`. The default is to autodetect the JMS - provider and its `ActivationSpec` class (see {api-spring-framework}/jms/listener/endpoint/DefaultJmsActivationSpecFactory.html[`DefaultJmsActivationSpecFactory`]). + provider and its `ActivationSpec` class (see {spring-framework-api}/jms/listener/endpoint/DefaultJmsActivationSpecFactory.html[`DefaultJmsActivationSpecFactory`]). | `destination-resolver` | A reference to the `DestinationResolver` strategy for resolving JMS `Destinations`. diff --git a/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc b/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc index ff47edf51210..ddd9c43d2e86 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc @@ -5,7 +5,7 @@ This describes how to receive messages with JMS in Spring. [[jms-receiving-sync]] -== Synchronous Reception +== Synchronous Receipt While JMS is typically associated with asynchronous processing, you can consume messages synchronously. The overloaded `receive(..)` methods provide this @@ -16,11 +16,11 @@ the receiver should wait before giving up waiting for a message. [[jms-receiving-async]] -== Asynchronous reception: Message-Driven POJOs +== Asynchronous Receipt: Message-Driven POJOs NOTE: Spring also supports annotated-listener endpoints through the use of the `@JmsListener` -annotation and provides an open infrastructure to register endpoints programmatically. -This is, by far, the most convenient way to setup an asynchronous receiver. +annotation and provides open infrastructure to register endpoints programmatically. +This is, by far, the most convenient way to set up an asynchronous receiver. See xref:integration/jms/annotated.adoc#jms-annotated-support[Enable Listener Endpoint Annotations] for more details. In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Message-Driven @@ -31,30 +31,7 @@ on multiple threads, it is important to ensure that your implementation is threa The following example shows a simple implementation of an MDP: -[source,java,indent=0,subs="verbatim,quotes"] ----- - import jakarta.jms.JMSException; - import jakarta.jms.Message; - import jakarta.jms.MessageListener; - import jakarta.jms.TextMessage; - - public class ExampleListener implements MessageListener { - - public void onMessage(Message message) { - if (message instanceof TextMessage textMessage) { - try { - System.out.println(textMessage.getText()); - } - catch (JMSException ex) { - throw new RuntimeException(ex); - } - } - else { - throw new IllegalArgumentException("Message must be of type TextMessage"); - } - } - } ----- +include-code::./ExampleListener[tag=snippet,indent=0] Once you have implemented your `MessageListener`, it is time to create a message listener container. @@ -62,21 +39,10 @@ container. The following example shows how to define and configure one of the message listener containers that ships with Spring (in this case, `DefaultMessageListenerContainer`): -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - ----- +include-code::./JmsConfiguration[tag=snippet,indent=0] See the Spring javadoc of the various message listener containers (all of which implement -{api-spring-framework}/jms/listener/MessageListenerContainer.html[MessageListenerContainer]) +{spring-framework-api}/jms/listener/MessageListenerContainer.html[MessageListenerContainer]) for a full description of the features supported by each implementation. @@ -123,19 +89,7 @@ messaging support. In a nutshell, it lets you expose almost any class as an MDP Consider the following interface definition: -[source,java,indent=0,subs="verbatim,quotes"] ----- - public interface MessageDelegate { - - void handleMessage(String message); - - void handleMessage(Map message); - - void handleMessage(byte[] message); - - void handleMessage(Serializable message); - } ----- +include-code::./MessageDelegate[tag=snippet,indent=0] Notice that, although the interface extends neither the `MessageListener` nor the `SessionAwareMessageListener` interface, you can still use it as an MDP by using the @@ -145,33 +99,13 @@ receive and handle. Now consider the following implementation of the `MessageDelegate` interface: -[source,java,indent=0,subs="verbatim,quotes"] ----- - public class DefaultMessageDelegate implements MessageDelegate { - // implementation elided for clarity... - } ----- +include-code::./DefaultMessageDelegate[tag=snippet,indent=0] In particular, note how the preceding implementation of the `MessageDelegate` interface (the `DefaultMessageDelegate` class) has no JMS dependencies at all. It truly is a POJO that we can make into an MDP through the following configuration: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - ----- +include-code::./JmsConfiguration[tag=snippet,indent=0] The next example shows another MDP that can handle only receiving JMS `TextMessage` messages. Notice how the message handling method is actually called @@ -181,38 +115,15 @@ also how the `receive(..)` method is strongly typed to receive and respond only `TextMessage` messages. The following listing shows the definition of the `TextMessageDelegate` interface: -[source,java,indent=0,subs="verbatim,quotes"] ----- - public interface TextMessageDelegate { - - void receive(TextMessage message); - } ----- +include-code::./TextMessageDelegate[tag=snippet,indent=0] The following listing shows a class that implements the `TextMessageDelegate` interface: -[source,java,indent=0,subs="verbatim,quotes"] ----- - public class DefaultTextMessageDelegate implements TextMessageDelegate { - // implementation elided for clarity... - } ----- +include-code::./DefaultTextMessageDelegate[tag=snippet,indent=0] The configuration of the attendant `MessageListenerAdapter` would then be as follows: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - ----- +include-code::./MessageListenerConfiguration[tag=snippet,indent=0] Note that, if the `messageListener` receives a JMS `Message` of a type other than `TextMessage`, an `IllegalStateException` is thrown (and subsequently @@ -220,21 +131,9 @@ swallowed). Another of the capabilities of the `MessageListenerAdapter` class is ability to automatically send back a response `Message` if a handler method returns a non-void value. Consider the following interface and class: -[source,java,indent=0,subs="verbatim,quotes"] ----- - public interface ResponsiveTextMessageDelegate { +include-code::./ResponsiveTextMessageDelegate[tag=snippet,indent=0] - // notice the return type... - String receive(TextMessage message); - } ----- - -[source,java,indent=0,subs="verbatim,quotes"] ----- - public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate { - // implementation elided for clarity... - } ----- +include-code::./DefaultResponsiveTextMessageDelegate[tag=snippet,indent=0] If you use the `DefaultResponsiveTextMessageDelegate` in conjunction with a `MessageListenerAdapter`, any non-null value that is returned from the execution of @@ -255,7 +154,7 @@ listener container. You can activate local resource transactions through the `sessionTransacted` flag on the listener container definition. Each message listener invocation then operates -within an active JMS transaction, with message reception rolled back in case of listener +within an active JMS transaction, with message receipt rolled back in case of listener execution failure. Sending a response message (through `SessionAwareMessageListener`) is part of the same local transaction, but any other resource operations (such as database access) operate independently. This usually requires duplicate message @@ -264,15 +163,7 @@ has committed but message processing failed to commit. Consider the following bean definition: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - ----- +include-code::./JmsConfiguration[tag=snippet,indent=0] To participate in an externally managed transaction, you need to configure a transaction manager and use a listener container that supports externally managed @@ -282,30 +173,15 @@ To configure a message listener container for XA transaction participation, you to configure a `JtaTransactionManager` (which, by default, delegates to the Jakarta EE server's transaction subsystem). Note that the underlying JMS `ConnectionFactory` needs to be XA-capable and properly registered with your JTA transaction coordinator. (Check your -Jakarta EE server's configuration of JNDI resources.) This lets message reception as well +Jakarta EE server's configuration of JNDI resources.) This lets message receipt as well as (for example) database access be part of the same transaction (with unified commit semantics, at the expense of XA transaction log overhead). The following bean definition creates a transaction manager: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +include-code::./ExternalTxJmsConfiguration[tag=transactionManagerSnippet,indent=0] Then we need to add it to our earlier container configuration. The container takes care of the rest. The following example shows how to do so: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - <1> - ----- -<1> Our transaction manager. - - - +include-code::./ExternalTxJmsConfiguration[tag=jmsContainerSnippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/integration/jms/using.adoc b/framework-docs/modules/ROOT/pages/integration/jms/using.adoc index db342992e03e..01babeefac6e 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/using.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/using.adoc @@ -167,13 +167,15 @@ operations that do not refer to a specific destination. One of the most common uses of JMS messages in the EJB world is to drive message-driven beans (MDBs). Spring offers a solution to create message-driven POJOs (MDPs) in a way -that does not tie a user to an EJB container. (See xref:integration/jms/receiving.adoc#jms-receiving-async[Asynchronous reception: Message-Driven POJOs] for detailed -coverage of Spring's MDP support.) Since Spring Framework 4.1, endpoint methods can be -annotated with `@JmsListener` -- see xref:integration/jms/annotated.adoc[Annotation-driven Listener Endpoints] for more details. +that does not tie a user to an EJB container. (See +xref:integration/jms/receiving.adoc#jms-receiving-async[Asynchronous Receipt: Message-Driven POJOs] +for detailed coverage of Spring's MDP support.) Endpoint methods can be annotated with +`@JmsListener` -- see xref:integration/jms/annotated.adoc[Annotation-driven Listener Endpoints] +for more details. A message listener container is used to receive messages from a JMS message queue and drive the `MessageListener` that is injected into it. The listener container is -responsible for all threading of message reception and dispatches into the listener for +responsible for all threading of message receipt and dispatches into the listener for processing. A message listener container is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, participating in transactions, resource acquisition and release, exception conversion, and so on. This @@ -227,14 +229,14 @@ the JMS provider, advanced functionality (such as participation in externally ma transactions), and compatibility with Jakarta EE environments. You can customize the cache level of the container. Note that, when no caching is enabled, -a new connection and a new session is created for each message reception. Combining this +a new connection and a new session is created for each message receipt. Combining this with a non-durable subscription with high loads may lead to message loss. Make sure to use a proper cache level in such a case. This container also has recoverable capabilities when the broker goes down. By default, a simple `BackOff` implementation retries every five seconds. You can specify a custom `BackOff` implementation for more fine-grained recovery options. See -{api-spring-framework}/util/backoff/ExponentialBackOff.html[`ExponentialBackOff`] for an example. +{spring-framework-api}/util/backoff/ExponentialBackOff.html[`ExponentialBackOff`] for an example. NOTE: Like its sibling (xref:integration/jms/using.adoc#jms-mdp-simple[`SimpleMessageListenerContainer`]), `DefaultMessageListenerContainer` supports native JMS transactions and allows for @@ -246,7 +248,7 @@ in the form of a business entity existence check or a protocol table check. Any such arrangements are significantly more efficient than the alternative: wrapping your entire processing with an XA transaction (through configuring your `DefaultMessageListenerContainer` with an `JtaTransactionManager`) to cover the -reception of the JMS message as well as the execution of the business logic in your +receipt of the JMS message as well as the execution of the business logic in your message listener (including database operations, etc.). IMPORTANT: The default `AUTO_ACKNOWLEDGE` mode does not provide proper reliability guarantees. diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/exporting.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/exporting.adoc index 2b1fb8663744..138c867a20c3 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/exporting.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/exporting.adoc @@ -5,63 +5,13 @@ The core class in Spring's JMX framework is the `MBeanExporter`. This class is responsible for taking your Spring beans and registering them with a JMX `MBeanServer`. For example, consider the following class: -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages",chomp="-packages"] ----- - package org.springframework.jmx; - - public class JmxTestBean implements IJmxTestBean { - - private String name; - private int age; - private boolean isSuperman; - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public void setName(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public int add(int x, int y) { - return x + y; - } - - public void dontExposeMe() { - throw new RuntimeException(); - } - } ----- +include-code::./JmxTestBean[tag=snippet,indent=0] To expose the properties and methods of this bean as attributes and operations of an MBean, you can configure an instance of the `MBeanExporter` class in your configuration file and pass in the bean, as the following example shows: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - ----- +include-code::./JmxConfiguration[tag=snippet,indent=0] The pertinent bean definition from the preceding configuration snippet is the `exporter` bean. The `beans` property tells the `MBeanExporter` exactly which of your beans must be diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/interface.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/interface.adoc index f9458765aac1..68e1c25206b0 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/interface.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/interface.adoc @@ -11,10 +11,10 @@ controlling the management interfaces of your beans. [[jmx-interface-assembler]] -== Using the `MBeanInfoAssembler` Interface +== Using the `MBeanInfoAssembler` API Behind the scenes, the `MBeanExporter` delegates to an implementation of the -`org.springframework.jmx.export.assembler.MBeanInfoAssembler` interface, which is +`org.springframework.jmx.export.assembler.MBeanInfoAssembler` API, which is responsible for defining the management interface of each bean that is exposed. The default implementation, `org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler`, @@ -28,35 +28,31 @@ or any arbitrary interface. [[jmx-interface-metadata]] == Using Source-level Metadata: Java Annotations -By using the `MetadataMBeanInfoAssembler`, you can define the management interfaces -for your beans by using source-level metadata. The reading of metadata is encapsulated -by the `org.springframework.jmx.export.metadata.JmxAttributeSource` interface. -Spring JMX provides a default implementation that uses Java annotations, namely -`org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource`. -You must configure the `MetadataMBeanInfoAssembler` with an implementation instance of -the `JmxAttributeSource` interface for it to function correctly (there is no default). +By using the `MetadataMBeanInfoAssembler`, you can define the management interfaces for +your beans by using source-level metadata. The reading of metadata is encapsulated by the +`org.springframework.jmx.export.metadata.JmxAttributeSource` interface. Spring JMX +provides a default implementation that uses Java annotations, namely +`org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource`. You must +configure the `MetadataMBeanInfoAssembler` with an implementation instance of the +`JmxAttributeSource` interface for it to function correctly, since there is no default. To mark a bean for export to JMX, you should annotate the bean class with the -`ManagedResource` annotation. You must mark each method you wish to expose as an operation -with the `ManagedOperation` annotation and mark each property you wish to expose -with the `ManagedAttribute` annotation. When marking properties, you can omit +`@ManagedResource` annotation. You must annotate each method you wish to expose as an +operation with the `@ManagedOperation` annotation and annotate each property you wish to +expose with the `@ManagedAttribute` annotation. When annotating properties, you can omit either the annotation of the getter or the setter to create a write-only or read-only attribute, respectively. -NOTE: A `ManagedResource`-annotated bean must be public, as must the methods exposing -an operation or an attribute. +NOTE: A `@ManagedResource`-annotated bean must be public, as must the methods exposing +operations or attributes. -The following example shows the annotated version of the `JmxTestBean` class that we -used in xref:integration/jmx/exporting.adoc#jmx-exporting-mbeanserver[Creating an MBeanServer]: +The following example shows an annotated version of the `JmxTestBean` class that we +used in xref:integration/jmx/exporting.adoc#jmx-exporting-mbeanserver[Creating an MBeanServer]. [source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- package org.springframework.jmx; - import org.springframework.jmx.export.annotation.ManagedResource; - import org.springframework.jmx.export.annotation.ManagedOperation; - import org.springframework.jmx.export.annotation.ManagedAttribute; - @ManagedResource( objectName="bean:name=testBean4", description="My Managed Bean", @@ -67,20 +63,20 @@ used in xref:integration/jmx/exporting.adoc#jmx-exporting-mbeanserver[Creating a persistPeriod=200, persistLocation="foo", persistName="bar") - public class AnnotationTestBean implements IJmxTestBean { + public class AnnotationTestBean { - private String name; private int age; - - @ManagedAttribute(description="The Age Attribute", currencyTimeLimit=15) - public int getAge() { - return age; - } + private String name; public void setAge(int age) { this.age = age; } + @ManagedAttribute(description="The Age Attribute", currencyTimeLimit=15) + public int getAge() { + return this.age; + } + @ManagedAttribute(description="The Name Attribute", currencyTimeLimit=20, defaultValue="bar", @@ -91,13 +87,12 @@ used in xref:integration/jmx/exporting.adoc#jmx-exporting-mbeanserver[Creating a @ManagedAttribute(defaultValue="foo", persistPeriod=300) public String getName() { - return name; + return this.name; } @ManagedOperation(description="Add two numbers") - @ManagedOperationParameters({ - @ManagedOperationParameter(name = "x", description = "The first number"), - @ManagedOperationParameter(name = "y", description = "The second number")}) + @ManagedOperationParameter(name = "x", description = "The first number") + @ManagedOperationParameter(name = "y", description = "The second number") public int add(int x, int y) { return x + y; } @@ -109,36 +104,37 @@ used in xref:integration/jmx/exporting.adoc#jmx-exporting-mbeanserver[Creating a } ---- -In the preceding example, you can see that the `JmxTestBean` class is marked with the -`ManagedResource` annotation and that this `ManagedResource` annotation is configured -with a set of properties. These properties can be used to configure various aspects +In the preceding example, you can see that the `AnnotationTestBean` class is annotated +with `@ManagedResource` and that this `@ManagedResource` annotation is configured +with a set of attributes. These attributes can be used to configure various aspects of the MBean that is generated by the `MBeanExporter` and are explained in greater -detail later in xref:integration/jmx/interface.adoc#jmx-interface-metadata-types[Source-level Metadata Types]. +detail later in xref:integration/jmx/interface.adoc#jmx-interface-metadata-types[Spring JMX Annotations]. -Both the `age` and `name` properties are annotated with the `ManagedAttribute` -annotation, but, in the case of the `age` property, only the getter is marked. +Both the `age` and `name` properties are annotated with `@ManagedAttribute`, +but, in the case of the `age` property, only the getter method is annotated. This causes both of these properties to be included in the management interface -as attributes, but the `age` attribute is read-only. +as managed attributes, but the `age` attribute is read-only. -Finally, the `add(int, int)` method is marked with the `ManagedOperation` attribute, +Finally, the `add(int, int)` method is annotated with `@ManagedOperation`, whereas the `dontExposeMe()` method is not. This causes the management interface to contain only one operation (`add(int, int)`) when you use the `MetadataMBeanInfoAssembler`. +NOTE: The `AnnotationTestBean` class is not required to implement any Java interfaces, +since the JMX management interface is derived solely from annotations. + The following configuration shows how you can configure the `MBeanExporter` to use the `MetadataMBeanInfoAssembler`: [source,xml,indent=0,subs="verbatim,quotes"] ---- + - - @@ -151,102 +147,116 @@ The following configuration shows how you can configure the `MBeanExporter` to u + + + ---- -In the preceding example, an `MetadataMBeanInfoAssembler` bean has been configured with an +In the preceding example, a `MetadataMBeanInfoAssembler` bean has been configured with an instance of the `AnnotationJmxAttributeSource` class and passed to the `MBeanExporter` through the assembler property. This is all that is required to take advantage of -metadata-driven management interfaces for your Spring-exposed MBeans. +annotation-driven management interfaces for your Spring-exposed MBeans. [[jmx-interface-metadata-types]] -== Source-level Metadata Types +== Spring JMX Annotations -The following table describes the source-level metadata types that are available for use in Spring JMX: +The following table describes the annotations that are available for use in Spring JMX: [[jmx-metadata-types]] -.Source-level metadata types +.Spring JMX annotations +[cols="1,1,3"] |=== -| Purpose| Annotation| Annotation Type +| Annotation | Applies to | Description -| Mark all instances of a `Class` as JMX managed resources. | `@ManagedResource` -| Class +| Classes +| Marks all instances of a `Class` as JMX managed resources. -| Mark a method as a JMX operation. -| `@ManagedOperation` -| Method +| `@ManagedNotification` +| Classes +| Indicates a JMX notification emitted by a managed resource. -| Mark a getter or setter as one half of a JMX attribute. | `@ManagedAttribute` -| Method (only getters and setters) +| Methods (only getters and setters) +| Marks a getter or setter as one half of a JMX attribute. -| Define descriptions for operation parameters. -| `@ManagedOperationParameter` and `@ManagedOperationParameters` -| Method +| `@ManagedMetric` +| Methods (only getters) +| Marks a getter as a JMX attribute, with added descriptor properties to indicate that it is a metric. + +| `@ManagedOperation` +| Methods +| Marks a method as a JMX operation. + +| `@ManagedOperationParameter` +| Methods +| Defines a description for an operation parameter. |=== -The following table describes the configuration parameters that are available for use on these source-level -metadata types: +The following table describes some of the common attributes that are available for use in +these annotations. Consult the Javadoc for each annotation for further details. [[jmx-metadata-parameters]] -.Source-level metadata parameters -[cols="1,3,1"] +.Spring JMX annotation attributes +[cols="1,1,3"] |=== -| Parameter | Description | Applies to +| Attribute | Applies to | Description -| `ObjectName` +| `objectName` +| `@ManagedResource` | Used by `MetadataNamingStrategy` to determine the `ObjectName` of a managed resource. -| `ManagedResource` | `description` -| Sets the friendly description of the resource, attribute or operation. -| `ManagedResource`, `ManagedAttribute`, `ManagedOperation`, or `ManagedOperationParameter` +| `@ManagedResource`, `@ManagedNotification`, `@ManagedAttribute`, `@ManagedMetric`, + `@ManagedOperation`, `@ManagedOperationParameter` +| Sets the description of the resource, notification, attribute, metric, or operation. | `currencyTimeLimit` +| `@ManagedResource`, `@ManagedAttribute`, `@ManagedMetric` | Sets the value of the `currencyTimeLimit` descriptor field. -| `ManagedResource` or `ManagedAttribute` | `defaultValue` +| `@ManagedAttribute` | Sets the value of the `defaultValue` descriptor field. -| `ManagedAttribute` | `log` +| `@ManagedResource` | Sets the value of the `log` descriptor field. -| `ManagedResource` | `logFile` +| `@ManagedResource` | Sets the value of the `logFile` descriptor field. -| `ManagedResource` | `persistPolicy` +| `@ManagedResource`, `@ManagedMetric` | Sets the value of the `persistPolicy` descriptor field. -| `ManagedResource` | `persistPeriod` +| `@ManagedResource`, `@ManagedMetric` | Sets the value of the `persistPeriod` descriptor field. -| `ManagedResource` | `persistLocation` +| `@ManagedResource` | Sets the value of the `persistLocation` descriptor field. -| `ManagedResource` | `persistName` +| `@ManagedResource` | Sets the value of the `persistName` descriptor field. -| `ManagedResource` | `name` +| `@ManagedOperationParameter` | Sets the display name of an operation parameter. -| `ManagedOperationParameter` | `index` +| `@ManagedOperationParameter` | Sets the index of an operation parameter. -| `ManagedOperationParameter` |=== @@ -255,14 +265,14 @@ metadata types: To simplify configuration even further, Spring includes the `AutodetectCapableMBeanInfoAssembler` interface, which extends the `MBeanInfoAssembler` -interface to add support for autodetection of MBean resources. If you configure the +interface to add support for auto-detection of MBean resources. If you configure the `MBeanExporter` with an instance of `AutodetectCapableMBeanInfoAssembler`, it is -allowed to "`vote`" on the inclusion of beans for exposure to JMX. +allowed to "vote" on the inclusion of beans for exposure to JMX. The only implementation of the `AutodetectCapableMBeanInfo` interface is the `MetadataMBeanInfoAssembler`, which votes to include any bean that is marked with the `ManagedResource` attribute. The default approach in this case is to use the -bean name as the `ObjectName`, which results in a configuration similar to the following: +bean name as the `ObjectName`, which results in configuration similar to the following: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -274,26 +284,29 @@ bean name as the `ObjectName`, which results in a configuration similar to the f - - - - - + + + + + ---- Notice that, in the preceding configuration, no beans are passed to the `MBeanExporter`. -However, the `JmxTestBean` is still registered, since it is marked with the `ManagedResource` -attribute and the `MetadataMBeanInfoAssembler` detects this and votes to include it. -The only problem with this approach is that the name of the `JmxTestBean` now has business -meaning. You can address this issue by changing the default behavior for `ObjectName` -creation as defined in xref:integration/jmx/naming.adoc[Controlling `ObjectName` Instances for Your Beans]. +However, the `AnnotationTestBean` is still registered, since it is annotated with +`@ManagedResource` and the `MetadataMBeanInfoAssembler` detects this and votes to include +it. The only downside with this approach is that the name of the `AnnotationTestBean` now +has business meaning. You can address this issue by configuring an `ObjectNamingStrategy` +as explained in xref:integration/jmx/naming.adoc[Controlling `ObjectName` Instances for +Your Beans]. You can also see an example which uses the `MetadataNamingStrategy` in +xref:integration/jmx/interface.adoc#jmx-interface-metadata[Using Source-level Metadata: Java Annotations]. + [[jmx-interface-java]] diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/naming.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/naming.adoc index 39b958281900..cdaf80432237 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/naming.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/naming.adoc @@ -122,25 +122,10 @@ your management interfaces, a convenience subclass of `MBeanExporter` is availab `namingStrategy`, `assembler`, and `attributeSource` configuration, since it always uses standard Java annotation-based metadata (autodetection is always enabled as well). In fact, rather than defining an `MBeanExporter` bean, an even -simpler syntax is supported by the `@EnableMBeanExport` `@Configuration` annotation, -as the following example shows: +simpler syntax is supported by the `@EnableMBeanExport` `@Configuration` annotation or the `` +element as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableMBeanExport - public class AppConfig { - - } ----- - -If you prefer XML-based configuration, the `` element serves the -same purpose and is shown in the following listing: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +include-code::./JmxConfiguration[tag=snippet,indent=0] If necessary, you can provide a reference to a particular MBean `server`, and the `defaultDomain` attribute (a property of `AnnotationMBeanExporter`) accepts an alternate @@ -148,21 +133,7 @@ value for the generated MBean `ObjectName` domains. This is used in place of the fully qualified package name as described in the previous section on xref:integration/jmx/naming.adoc#jmx-naming-metadata[MetadataNamingStrategy], as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @EnableMBeanExport(server="myMBeanServer", defaultDomain="myDomain") - @Configuration - ContextConfiguration { - - } ----- - -The following example shows the XML equivalent of the preceding annotation-based example: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +include-code::./CustomJmxConfiguration[tag=snippet,indent=0] CAUTION: Do not use interface-based AOP proxies in combination with autodetection of JMX annotations in your bean classes. Interface-based proxies "`hide`" the target class, which diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc index 78e197acbbb4..4811434a9c70 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc @@ -246,7 +246,7 @@ instance. The `NotificationPublisherAware` interface supplies an instance of a which the bean can then use to publish `Notifications`. As stated in the javadoc of the -{api-spring-framework}/jmx/export/notification/NotificationPublisher.html[`NotificationPublisher`] +{spring-framework-api}/jmx/export/notification/NotificationPublisher.html[`NotificationPublisher`] interface, managed beans that publish events through the `NotificationPublisher` mechanism are not responsible for the state management of notification listeners. Spring's JMX support takes care of handling all the JMX infrastructure issues. diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc index 369ccbf2c19c..7e6164bc86e6 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc @@ -6,10 +6,8 @@ This section contains links to further resources about JMX: * The https://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html[JMX homepage] at Oracle. -* The https://jcp.org/aboutJava/communityprocess/final/jsr003/index3.html[JMX - specification] (JSR-000003). -* The https://jcp.org/aboutJava/communityprocess/final/jsr160/index.html[JMX Remote API - specification] (JSR-000160). +* The {JSR}003[JMX specification] (JSR-000003). +* The {JSR}160[JMX Remote API specification] (JSR-000160). * The http://mx4j.sourceforge.net/[MX4J homepage]. (MX4J is an open-source implementation of various JMX specs.) diff --git a/framework-docs/modules/ROOT/pages/integration/observability.adoc b/framework-docs/modules/ROOT/pages/integration/observability.adoc index 62fa47ab0983..54cfa9e03bbc 100644 --- a/framework-docs/modules/ROOT/pages/integration/observability.adoc +++ b/framework-docs/modules/ROOT/pages/integration/observability.adoc @@ -1,13 +1,13 @@ [[observability]] = Observability Support -Micrometer defines an https://micrometer.io/docs/observation[Observation concept that enables both Metrics and Traces] in applications. +Micrometer defines an {micrometer-docs}/observation.html[Observation concept that enables both Metrics and Traces] in applications. Metrics support offers a way to create timers, gauges, or counters for collecting statistics about the runtime behavior of your application. Metrics can help you to track error rates, usage patterns, performance, and more. Traces provide a holistic view of an entire system, crossing application boundaries; you can zoom in on particular user requests and follow their entire completion across applications. Spring Framework instruments various parts of its own codebase to publish observations if an `ObservationRegistry` is configured. -You can learn more about {docs-spring-boot}/html/actuator.html#actuator.metrics[configuring the observability infrastructure in Spring Boot]. +You can learn more about {spring-boot-docs-ref}/actuator/observability.html[configuring the observability infrastructure in Spring Boot]. [[observability.list]] @@ -37,8 +37,8 @@ As outlined xref:integration/observability.adoc[at the beginning of this section |Processing time for an execution of a `@Scheduled` task |=== -NOTE: Observations are using Micrometer's official naming convention, but Metrics names will be automatically converted -https://micrometer.io/docs/concepts#_naming_meters[to the format preferred by the monitoring system backend] +NOTE: Observations use Micrometer's official naming convention, but Metrics names will be automatically converted +{micrometer-docs}/concepts/naming.html[to the format preferred by the monitoring system backend] (Prometheus, Atlas, Graphite, InfluxDB...). @@ -97,7 +97,7 @@ This can be done by declaring a `SchedulingConfigurer` bean that sets the observ include-code::./ObservationSchedulingConfigurer[] -It is using the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`. +It uses the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`. You can configure a custom implementation on the `ObservationRegistry` directly. During the execution of the scheduled method, the current observation is restored in the `ThreadLocal` context or the Reactor context (if the scheduled method returns a `Mono` or `Flux` type). @@ -107,9 +107,10 @@ By default, the following `KeyValues` are created: [cols="a,a"] |=== |Name | Description -|`code.function` _(required)_|Name of Java `Method` that is scheduled for execution. -|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method. -|`exception` _(required)_|Name of the exception thrown during the execution, or `"none"` if no exception happened. +|`code.function` _(required)_|Name of the Java `Method` that is scheduled for execution. +|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method, or `"ANONYMOUS"` for anonymous classes. +|`error` _(required)_|Class name of the exception thrown during the execution, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. |`outcome` _(required)_|Outcome of the method execution. Can be `"SUCCESS"`, `"ERROR"` or `"UNKNOWN"` (if for example the operation was cancelled during execution). |=== @@ -125,7 +126,7 @@ This instrumentation will create 2 types of observations: * `"jms.message.publish"` when a JMS message is sent to the broker, typically with `JmsTemplate`. * `"jms.message.process"` when a JMS message is processed by the application, typically with a `MessageListener` or a `@JmsListener` annotated method. -NOTE: currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the reception of a message. +NOTE: Currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the receipt of a message. Such an integration would typically instrument `MessageConsumer#receive` method calls. But once those return, the processing time is not measured and the trace scope cannot be propagated to the application. By default, both observations share the same set of possible `KeyValues`: @@ -134,9 +135,10 @@ By default, both observations share the same set of possible `KeyValues`: [cols="a,a"] |=== |Name | Description -|`exception` |Class name of the exception thrown during the messaging operation (or "none"). +|`error` |Class name of the exception thrown during the messaging operation (or "none"). +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. |`messaging.destination.temporary` _(required)_|Whether the destination is a `TemporaryQueue` or `TemporaryTopic` (values: `"true"` or `"false"`). -|`messaging.operation` _(required)_|Name of JMS operation being performed (values: `"publish"` or `"process"`). +|`messaging.operation` _(required)_|Name of the JMS operation being performed (values: `"publish"` or `"process"`). |=== .High cardinality Keys @@ -144,7 +146,7 @@ By default, both observations share the same set of possible `KeyValues`: |=== |Name | Description |`messaging.message.conversation_id` |The correlation ID of the JMS message. -|`messaging.destination.name` |The name of destination the current message was sent to. +|`messaging.destination.name` |The name of the destination the current message was sent to. |`messaging.message.id` |Value used by the messaging system as an identifier for the message. |=== @@ -160,6 +162,8 @@ include-code::./JmsTemplatePublish[] It uses the `io.micrometer.jakarta9.instrument.jms.DefaultJmsPublishObservationConvention` by default, backed by the `io.micrometer.jakarta9.instrument.jms.JmsPublishObservationContext`. +Similar observations are recorded with `@JmsListener` annotated methods when response messages are returned from the listener method. + [[observability.jms.process]] === JMS message Processing instrumentation @@ -207,8 +211,9 @@ By default, the following `KeyValues` are created: [cols="a,a"] |=== |Name | Description -|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened. -|`method` _(required)_|Name of HTTP request method or `"none"` if the request was not received properly. +|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. +|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method. |`outcome` _(required)_|Outcome of the HTTP server exchange. |`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created. |`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests. @@ -230,10 +235,10 @@ This can be done on the `WebHttpHandlerBuilder`, as follows: include-code::./HttpHandlerConfiguration[] -It is using the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`. +It uses the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`. This will only record an observation as an error if the `Exception` has not been handled by an application Controller. -Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and <> will not be recorded with the observation. +Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and xref:web/webflux/ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation. You can, at any point during request processing, set the error field on the `ObservationContext` yourself: include-code::./UserController[] @@ -244,8 +249,9 @@ By default, the following `KeyValues` are created: [cols="a,a"] |=== |Name | Description -|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened. -|`method` _(required)_|Name of HTTP request method or `"none"` if the request was not received properly. +|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. +|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method. |`outcome` _(required)_|Outcome of the HTTP server exchange. |`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created. |`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests. @@ -264,6 +270,7 @@ By default, the following `KeyValues` are created: == HTTP Client Instrumentation HTTP client exchange observations are created with the name `"http.client.requests"` for blocking and reactive clients. +This observation measures the entire HTTP request/response exchange, from connection establishment up to body deserialization. Unlike their server counterparts, the instrumentation is implemented directly in the client so the only required step is to configure an `ObservationRegistry` on the client. [[observability.http-client.resttemplate]] @@ -278,12 +285,13 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ [cols="a,a"] |=== |Name | Description -|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created. -|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered. +|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method. +|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not considered. |`client.name` _(required)_|Client name derived from the request URI host. |`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received. |`outcome` _(required)_|Outcome of the HTTP client exchange. -|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. |=== .High cardinality Keys @@ -305,12 +313,13 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ [cols="a,a"] |=== |Name | Description -|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created. -|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered. +|`method` _(required)_|Name of the HTTP request method or `"none"` if the request could not be created. +|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not considered. |`client.name` _(required)_|Client name derived from the request URI host. |`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received. |`outcome` _(required)_|Outcome of the HTTP client exchange. -|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. |=== .High cardinality Keys @@ -324,7 +333,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ [[observability.http-client.webclient]] === WebClient -Applications must configure an `ObservationRegistry` on the `WebClient` builder to enable the instrumentation; without that, observations are "no-ops". +Applications must configure an `ObservationRegistry` on the `WebClient.Builder` to enable the instrumentation; without that, observations are "no-ops". Spring Boot will auto-configure `WebClient.Builder` beans with the observation registry already set. Instrumentation uses the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`. @@ -333,12 +342,13 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien [cols="a,a"] |=== |Name | Description -|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created. -|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered. +|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method. +|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not considered. |`client.name` _(required)_|Client name derived from the request URI host. |`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received. |`outcome` _(required)_|Outcome of the HTTP client exchange. -|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened. +|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future. |=== .High cardinality Keys @@ -359,7 +369,7 @@ This means that during the execution of that task, the ThreadLocals and logging If the application globally configures a custom `ApplicationEventMulticaster` with a strategy that schedules event processing on different threads, this is no longer true. All `@EventListener` methods will be processed on a different thread, outside the main event publication thread. -In these cases, the https://micrometer.io/docs/contextPropagation[Micrometer Context Propagation library] can help propagate such values and better correlate the processing of the events. +In these cases, the {micrometer-context-propagation-docs}/[Micrometer Context Propagation library] can help propagate such values and better correlate the processing of the events. The application can configure the chosen `TaskExecutor` to use a `ContextPropagatingTaskDecorator` that decorates tasks and propagates context. For this to work, the `io.micrometer:context-propagation` library must be present on the classpath: diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index 0ce1a984b00d..d6a143eab1e9 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -13,12 +13,12 @@ The Spring Framework provides the following choices for making calls to REST end == `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 Java object to HTTP request, and creation of objects from the HTTP response. +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. === Creating 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, a default request headers, or `uriBuilderFactory`, or registering interceptors and initializers. +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. Once created (or built), the `RestClient` can be used safely by multiple threads. @@ -28,7 +28,7 @@ The following sample shows how to create a default `RestClient`, and how to buil ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- RestClient defaultClient = RestClient.create(); @@ -38,6 +38,7 @@ RestClient customClient = RestClient.builder() .baseUrl("https://example.com") .defaultUriVariables(Map.of("variable", "foo")) .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") .requestInterceptor(myCustomInterceptor) .requestInitializer(myCustomInitializer) .build(); @@ -45,16 +46,17 @@ RestClient customClient = RestClient.builder() Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- val defaultClient = RestClient.create() val customClient = RestClient.builder() .requestFactory(HttpComponentsClientHttpRequestFactory()) - .messageConverters(converters -> converters.add(MyCustomMessageConverter())) + .messageConverters { converters -> converters.add(MyCustomMessageConverter()) } .baseUrl("https://example.com") - .defaultUriVariables(Map.of("variable", "foo")) + .defaultUriVariables(mapOf("variable" to "foo")) .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") .requestInterceptor(myCustomInterceptor) .requestInitializer(myCustomInitializer) .build() @@ -64,41 +66,72 @@ val customClient = RestClient.builder() === Using 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. +This can be done with `method(HttpMethod)` or with the convenience methods `get()`, `head()`, `post()`, and so on. ==== 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. -The URL is typically specified as `String`, with optional URI template variables. -String URLs are encoded by default, but this can be changed by building a client with a custom `uriBuilderFactory`. +This step is optional and can be skipped if the `RestClient` is configured with a default URI. +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 URL can also be provided with a function, or as `java.net.URI`, both of which are not encoded. +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- +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) + ... +---- +====== + +A function can also be used for more controls, such as specifying xref:web/webmvc/mvc-uri-building.adoc[request parameters]. + +String URLs are encoded by default, but this can be changed by building a client with a custom `uriBuilderFactory`. +The URL can also be provided with a function or as a `java.net.URI`, both of which are not encoded. For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri-building.adoc[URI Links]. ==== Request headers and body -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 request that can contain a body (`POST`, `PUT`, and `PATCH`), additional methods are available: `contentType(MediaType)`, and `contentLength(long)`. +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)`. 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. Finally, the body can be set to a callback function that writes to an `OutputStream`. ==== Retrieving the response -Once the request has been set up, the HTTP response is accessed by invoking `retrieve()`. -The response body can be accessed by using `body(Class)`, or `body(ParameterizedTypeReference)` for parameterized types like lists. -The `body` method converts the response contents into various types, for instance bytes can be converted into a `String`, JSON into objects using Jackson, and so on (see <>). -The response can also be converted into a `ResponseEntity`, giving access to the response headers as well as the body. +Once the request has been set up, it can be sent by chaining method calls after `retrieve()`. +For example, the response body can be accessed by using `retrieve().body(Class)` or `retrieve().body(ParameterizedTypeReference)` for parameterized types like lists. +The `body` method converts the response contents into various types – for instance, bytes can be converted into a `String`, JSON can be converted into objects using Jackson, and so on (see <>). -This sample shows how `RestClient` can be used to perform a simple GET request. +The response can also be converted into a `ResponseEntity`, giving access to the response headers as well as the body, with `retrieve().toEntity(Class)` + +NOTE: Calling `retrieve()` by itself is a no-op and returns a `ResponseSpec`. +Applications must invoke a terminal operation on the `ResponseSpec` to have any side effect. +If consuming the response has no interest for your use case, you can use `retrieve().toBodilessEntity()`. + +This sample shows how `RestClient` can be used to perform a simple `GET` request. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String result = restClient.get() <1> .uri("https://example.com") <2> @@ -115,7 +148,7 @@ System.out.println(result); <5> Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val result= restClient.get() <1> .uri("https://example.com") <2> @@ -137,7 +170,7 @@ Access to the response status code and headers is provided through `ResponseEnti ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ResponseEntity result = restClient.get() <1> .uri("https://example.com") <1> @@ -154,7 +187,7 @@ System.out.println("Contents: " + result.getBody()); <3> Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val result = restClient.get() <1> .uri("https://example.com") <1> @@ -171,13 +204,13 @@ println("Contents: " + result.body) <3> ====== `RestClient` can convert JSON to objects, using the Jackson library. -Note the usage of uri variables in this sample, and that the `Accept` header is set to JSON. +Note the usage of URI variables in this sample and that the `Accept` header is set to JSON. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- int id = ...; Pet pet = restClient.get() @@ -192,7 +225,7 @@ Pet pet = restClient.get() Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val id = ... val pet = restClient.get() @@ -212,7 +245,7 @@ In the next sample, `RestClient` is used to perform a POST request that contains ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Pet pet = ... <1> ResponseEntity response = restClient.post() <2> @@ -230,7 +263,7 @@ ResponseEntity response = restClient.post() <2> Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val pet: Pet = ... <1> val response = restClient.post() <2> @@ -248,20 +281,21 @@ val response = restClient.post() <2> ====== ==== Error handling + By default, `RestClient` throws a subclass of `RestClientException` when retrieving a response with a 4xx or 5xx status code. -This behavior can be overriden using `onStatus`. +This behavior can be overridden using `onStatus`. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[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> + throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3> }) .body(String.class); ---- @@ -271,7 +305,7 @@ String result = restClient.get() <1> Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val result = restClient.get() <1> .uri("https://example.com/this-url-does-not-exist") <1> @@ -286,14 +320,15 @@ val result = restClient.get() <1> ====== ==== Exchange -For more advanced scenarios, the `RestClient` gives access to the underlying HTTP request and response through the `exchange` method, which can be used instead of `retrieve()`. -Status handlers are not applied when you exchange, because the exchange function already provides access to the full response, allowing you to perform any error handling necessary. + +For more advanced scenarios, the `RestClient` gives access to the underlying HTTP request and response through the `exchange()` method, which can be used instead of `retrieve()`. +Status handlers are not applied when use `exchange()`, because the exchange function already provides access to the full response, allowing you to perform any error handling necessary. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Pet result = restClient.get() .uri("https://petclinic.example.com/pets/{id}", id) @@ -314,7 +349,7 @@ Pet result = restClient.get() Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val result = restClient.get() .uri("https://petclinic.example.com/pets/{id}", id) @@ -336,72 +371,12 @@ val result = restClient.get() [[rest-message-conversion]] === HTTP Message Conversion -[.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs[See equivalent in the Reactive stack]# -The `spring-web` module contains the `HttpMessageConverter` interface for reading and writing the body of HTTP requests and responses through `InputStream` and `OutputStream`. -`HttpMessageConverter` instances are used on the client side (for example, in the `RestClient`) and on the server side (for example, in Spring MVC REST controllers). - -Concrete implementations for the main media (MIME) types are provided in the framework and are, by default, registered with the `RestClient` and `RestTemplate` on the client side and with `RequestMappingHandlerAdapter` on the server side (see xref:web/webmvc/mvc-config/message-converters.adoc[Configuring Message Converters]). - -Several implementations of `HttpMessageConverter` are described below. -Refer to the {api-spring-framework}/http/converter/HttpMessageConverter.html[`HttpMessageConverter` Javadoc] for the complete list. -For all converters, a default media type is used, but you can override it by setting the `supportedMediaTypes` property. - -[[rest-message-converters-tbl]] -.HttpMessageConverter Implementations -[cols="1,3"] -|=== -| MessageConverter | Description - -| `StringHttpMessageConverter` -| An `HttpMessageConverter` implementation that can read and write `String` instances from the HTTP request and response. -By default, this converter supports all text media types(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`. - -| `FormHttpMessageConverter` -| An `HttpMessageConverter` implementation that can read and write form data from the HTTP request and response. -By default, this converter reads and writes the `application/x-www-form-urlencoded` media type. -Form data is read from and written into a `MultiValueMap`. -The converter can also write (but not read) multipart data read from a `MultiValueMap`. -By default, `multipart/form-data` is supported. -Additional multipart subtypes can be supported for writing form data. -Consult the javadoc for `FormHttpMessageConverter` for further details. - -| `ByteArrayHttpMessageConverter` -| An `HttpMessageConverter` implementation that can read and write byte arrays from the HTTP request and response. -By default, this converter supports all media types (`{asterisk}/{asterisk}`) and writes with a `Content-Type` of `application/octet-stream`. -You can override this by setting the `supportedMediaTypes` property and overriding `getContentType(byte[])`. - -| `MarshallingHttpMessageConverter` -| An `HttpMessageConverter` implementation that can read and write XML by using Spring's `Marshaller` and `Unmarshaller` abstractions from the `org.springframework.oxm` package. -This converter requires a `Marshaller` and `Unmarshaller` before it can be used. -You can inject these through constructor or bean properties. -By default, this converter supports `text/xml` and `application/xml`. - -| `MappingJackson2HttpMessageConverter` -| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `ObjectMapper`. -You can customize JSON mapping as needed through the use of Jackson's provided annotations. -When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `ObjectMapper` through the `ObjectMapper` property. -By default, this converter supports `application/json`. - -| `MappingJackson2XmlHttpMessageConverter` -| An `HttpMessageConverter` implementation that can read and write XML by using https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML] extension's `XmlMapper`. -You can customize XML mapping as needed through the use of JAXB or Jackson's provided annotations. -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`. - -| `SourceHttpMessageConverter` -| An `HttpMessageConverter` implementation that can read and write `javax.xml.transform.Source` from the HTTP request and response. -Only `DOMSource`, `SAXSource`, and `StreamSource` are supported. -By default, this converter supports `text/xml` and `application/xml`. - -|=== - -By default, `RestClient` and `RestTemplate` register all built-in message converters, depending on the availability of underlying libraries on the classpath. -You can also set the message converters to use explicitly, by using `messageConverters` on the `RestClient` builder, or via the `messageConverters` property of `RestTemplate`. +xref:web/webmvc/message-converters.adoc#message-converters[See the supported HTTP message converters in the dedicated section]. ==== Jackson JSON Views -To serialize only a subset of the object properties, you can specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON View], as the following example shows: +To serialize only a subset of the object properties, you can specify a {baeldung-blog}/jackson-json-view-annotation[Jackson JSON View], as the following example shows: [source,java,indent=0,subs="verbatim"] ---- @@ -437,13 +412,13 @@ parts.add("xmlPart", new HttpEntity<>(myBean, headers)); ---- 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. +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`. +If the `MultiValueMap` has `String` values, the `Content-Type` defaults to `application/x-www-form-urlencoded`. If necessary the `Content-Type` may also be set explicitly. [[rest-request-factories]] @@ -453,17 +428,20 @@ To execute the HTTP request, `RestClient` uses a client HTTP library. These libraries are adapted via the `ClientRequestFactory` interface. Various implementations are available: -* `JdkClientHttpRequestFactory` for Java's `HttpClient`, -* `HttpComponentsClientHttpRequestFactory` for use with Apache HTTP Components `HttpClient`, -* `JettyClientHttpRequestFactory` for Jetty's `HttpClient`, -* `ReactorNettyClientRequestFactory` for Reactor Netty's `HttpClient`, -* `SimpleClientHttpRequestFactory` as a simple default. +* `JdkClientHttpRequestFactory` for Java's `HttpClient` +* `HttpComponentsClientHttpRequestFactory` for use with Apache HTTP Components `HttpClient` +* `JettyClientHttpRequestFactory` for Jetty's `HttpClient` +* `ReactorNettyClientRequestFactory` for Reactor Netty's `HttpClient` +* `SimpleClientHttpRequestFactory` as a simple default If no request factory is specified when the `RestClient` was built, it will use the Apache or Jetty `HttpClient` if they are available on the classpath. Otherwise, if the `java.net.http` module is loaded, it will use Java's `HttpClient`. Finally, it will resort to the simple default. +TIP: Note that the `SimpleClientHttpRequestFactory` may raise an exception when accessing the status of a response that represents an error (for example, 401). +If this is an issue, use any of the alternative request factories. + [[rest-webclient]] == `WebClient` @@ -473,12 +451,12 @@ synchronous, asynchronous, and streaming scenarios. `WebClient` supports the following: -* Non-blocking I/O. -* Reactive Streams back pressure. -* High concurrency with fewer hardware resources. -* Functional-style, fluent API that takes advantage of Java 8 lambdas. -* Synchronous and asynchronous interactions. -* Streaming up to or streaming down from a server. +* Non-blocking I/O +* Reactive Streams back pressure +* High concurrency with fewer hardware resources +* Functional-style, fluent API that takes advantage of Java 8 lambdas +* Synchronous and asynchronous interactions +* Streaming up to or streaming down from a server See xref:web/webflux-webclient.adoc[WebClient] for more details. @@ -848,17 +826,17 @@ It can be used to migrate from the latter to the former. .toEntity(ParameterizedTypeReference)` footnote:request-entity[] -| `execute(String, HttpMethod method, RequestCallback, ResponseExtractor, Object...)` +| `execute(String, HttpMethod, RequestCallback, ResponseExtractor, Object...)` | `method(HttpMethod) .uri(String, Object...) .exchange(ExchangeFunction)` -| `execute(String, HttpMethod method, RequestCallback, ResponseExtractor, Map)` +| `execute(String, HttpMethod, RequestCallback, ResponseExtractor, Map)` | `method(HttpMethod) .uri(String, Map) .exchange(ExchangeFunction)` -| `execute(URI, HttpMethod method, RequestCallback, ResponseExtractor)` +| `execute(URI, HttpMethod, RequestCallback, ResponseExtractor)` | `method(HttpMethod) .uri(URI) .exchange(ExchangeFunction)` @@ -879,7 +857,7 @@ Start by creating the interface with `@HttpExchange` methods: [source,java,indent=0,subs="verbatim,quotes"] ---- - interface RepositoryService { + public interface RepositoryService { @GetExchange("/repos/{owner}/{repo}") Repository getRepository(@PathVariable String owner, @PathVariable String repo); @@ -906,8 +884,8 @@ For `WebClient`: [source,java,indent=0,subs="verbatim,quotes"] ---- - WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build(); - WebClientAdapter adapter = WebClientAdapter.forClient(webClient) + 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); @@ -930,7 +908,7 @@ For `RestTemplate`: [source,java,indent=0,subs="verbatim,quotes"] ---- @HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json") - interface RepositoryService { + public interface RepositoryService { @GetExchange Repository getRepository(@PathVariable String owner, @PathVariable String repo); @@ -964,15 +942,20 @@ method parameters: | Dynamically set the HTTP method for the request, overriding the annotation's `method` attribute | `@RequestHeader` -| Add a request header or multiple headers. The argument may be a `Map` or - `MultiValueMap` with multiple headers, a `Collection` of values, or an - individual value. Type conversion is supported for non-String values. +| Add a request header or multiple headers. The argument may be a single value, + a `Collection` of values, `Map`,`MultiValueMap`. + Type conversion is supported for non-String values. Header values are added and + do not override already added header values. | `@PathVariable` | Add a variable for expand a placeholder in the request URL. The argument may be a `Map` with multiple variables, or an individual value. Type conversion is supported for non-String values. +| `@RequestAttribute` +| Provide an `Object` to add as a request attribute. Only supported by `RestClient` + and `WebClient`. + | `@RequestBody` | Provide the body of the request either as an Object to be serialized, or a Reactive Streams `Publisher` such as `Mono`, `Flux`, or any other async type supported @@ -989,7 +972,7 @@ method parameters: | `@RequestPart` | Add a request part, which may be a String (form field), `Resource` (file part), - Object (entity to be encoded, e.g. as JSON), `HttpEntity` (part content and headers), + Object (entity to be encoded, for example, as JSON), `HttpEntity` (part content and headers), a Spring `Part`, or Reactive Streams `Publisher` of any of the above. | `MultipartFile` @@ -1003,6 +986,32 @@ method parameters: |=== +Method parameters cannot be `null` unless the `required` attribute (where available on a +parameter annotation) is set to `false`, or the parameter is marked optional as determined by +{spring-framework-api}/core/MethodParameter.html#isOptional()[`MethodParameter#isOptional`]. + + + +[[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. + +In the following HTTP interface, we are using a custom `Search` type as a parameter: + +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. + +include-code::./CustomHttpServiceArgumentResolver[tag=argumentresolver,indent=0] + +Finally, we can use this argument resolver during the setup and use our HTTP interface. + +include-code::./CustomHttpServiceArgumentResolver[tag=usage,indent=0] [[rest-http-interface-return-values]] === Return Values @@ -1078,11 +1087,34 @@ underlying HTTP client, which operates at a lower level and provides more contro [[rest-http-interface-exceptions]] -=== Exception Handling +=== Error Handling + +To customize error response handling, you need to configure the underlying HTTP client. + +For `RestClient`: + +By default, `RestClient` raises `RestClientException` for 4xx and 5xx HTTP status codes. +To customize this, register a response status handler that applies to all responses +performed through the client: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + RestClient restClient = RestClient.builder() + .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...) + .build(); + + RestClientAdapter adapter = RestClientAdapter.create(restClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); +---- -By default, `WebClient` raises `WebClientResponseException` for 4xx and 5xx HTTP status -codes. To customize this, you can register a response status handler that applies to all -responses performed through the client: +For more details and options, such as suppressing error status codes, see the Javadoc of +`defaultStatusHandler` in `RestClient.Builder`. + +For `WebClient`: + +By default, `WebClient` raises `WebClientResponseException` for 4xx and 5xx HTTP status codes. +To customize this, register a response status handler that applies to all responses +performed through the client: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -1090,10 +1122,28 @@ responses performed through the client: .defaultStatusHandler(HttpStatusCode::isError, resp -> ...) .build(); - WebClientAdapter clientAdapter = WebClientAdapter.forClient(webClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory - .builder(clientAdapter).build(); + WebClientAdapter adapter = WebClientAdapter.create(webClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(adapter).build(); ---- For more details and options, such as suppressing error status codes, see the Javadoc of `defaultStatusHandler` in `WebClient.Builder`. + +For `RestTemplate`: + +By default, `RestTemplate` raises `RestClientException` for 4xx and 5xx HTTP status codes. +To customize this, register an error handler that applies to all responses +performed through the client: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setErrorHandler(myErrorHandler); + + 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. + diff --git a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc index 344e365d34ae..a031ee208f81 100644 --- a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc +++ b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc @@ -50,6 +50,9 @@ The variants that Spring provides are as follows: for each invocation. However, it does support a concurrency limit that blocks any invocations that are over the limit until a slot has been freed up. If you are looking for true pooling, see `ThreadPoolTaskExecutor`, later in this list. + This will use JDK 21's Virtual Threads, when the "virtualThreads" + option is enabled. This implementation also supports graceful shutdown through + Spring's lifecycle management. * `ConcurrentTaskExecutor`: This implementation is an adapter for a `java.util.concurrent.Executor` instance. There is an alternative (`ThreadPoolTaskExecutor`) that exposes the `Executor` @@ -61,15 +64,13 @@ The variants that Spring provides are as follows: a `java.util.concurrent.ThreadPoolExecutor` and wraps it in a `TaskExecutor`. If you need to adapt to a different kind of `java.util.concurrent.Executor`, we recommend that you use a `ConcurrentTaskExecutor` instead. + It also provides a pause/resume capability and graceful shutdown through + Spring's lifecycle management. * `DefaultManagedTaskExecutor`: This implementation uses a JNDI-obtained `ManagedExecutorService` in a JSR-236 compatible runtime environment (such as a Jakarta EE application server), replacing a CommonJ WorkManager for that purpose. -As of 6.1, `ThreadPoolTaskExecutor` provides a pause/resume capability and graceful -shutdown through Spring's lifecycle management. There is also a new "virtualThreads" -option on `SimpleAsyncTaskExecutor` which is aligned with JDK 21's Virtual Threads, -as well as a graceful shutdown capability for `SimpleAsyncTaskExecutor` as well. [[scheduling-task-executor-usage]] @@ -79,38 +80,7 @@ Spring's `TaskExecutor` implementations are commonly used with dependency inject In the following example, we define a bean that uses the `ThreadPoolTaskExecutor` to asynchronously print out a set of messages: -[source,java,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.core.task.TaskExecutor; - - public class TaskExecutorExample { - - private class MessagePrinterTask implements Runnable { - - private String message; - - public MessagePrinterTask(String message) { - this.message = message; - } - - public void run() { - System.out.println(message); - } - } - - private TaskExecutor taskExecutor; - - public TaskExecutorExample(TaskExecutor taskExecutor) { - this.taskExecutor = taskExecutor; - } - - public void printMessages() { - for(int i = 0; i < 25; i++) { - taskExecutor.execute(new MessagePrinterTask("Message" + i)); - } - } - } ----- +include-code::./TaskExecutorExample[tag=snippet,indent=0] As you can see, rather than retrieving a thread from the pool and executing it yourself, you add your `Runnable` to the queue. Then the `TaskExecutor` uses its internal rules to @@ -118,19 +88,23 @@ decide when the task gets run. To configure the rules that the `TaskExecutor` uses, we expose simple bean properties: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - +include-code::./TaskExecutorConfiguration[tag=snippet,indent=0] - - - ----- +Most `TaskExecutor` implementations provide a way to automatically wrap tasks submitted +with a `TaskDecorator`. Decorators should delegate to the task it is wrapping, possibly +implementing custom behavior before/after the execution of the task. + +Let's consider a simple implementation that will log messages before and after the execution +or our tasks: + +include-code::./LoggingTaskDecorator[indent=0] +We can then configure our decorator on a `TaskExecutor` instance: + +include-code::./TaskExecutorConfiguration[tag=decorator,indent=0] + +In case multiple decorators are needed, the `org.springframework.core.task.support.CompositeTaskDecorator` +can be used to execute sequentially multiple decorators. [[scheduling-task-scheduler]] @@ -252,7 +226,9 @@ application server environments, as well -- in particular on Tomcat and Jetty. As of 6.1, `ThreadPoolTaskScheduler` provides a pause/resume capability and graceful shutdown through Spring's lifecycle management. There is also a new option called `SimpleAsyncTaskScheduler` which is aligned with JDK 21's Virtual Threads, using a -single scheduler thread but firing up a new thread for every scheduled task execution. +single scheduler thread but firing up a new thread for every scheduled task execution +(except for fixed-delay tasks which all operate on a single scheduler thread, so for +this virtual-thread-aligned option, fixed rates and cron triggers are recommended). @@ -267,35 +243,19 @@ execution. === Enable Scheduling Annotations To enable support for `@Scheduled` and `@Async` annotations, you can add `@EnableScheduling` -and `@EnableAsync` to one of your `@Configuration` classes, as the following example shows: +and `@EnableAsync` to one of your `@Configuration` classes, or `` element, +as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableAsync - @EnableScheduling - public class AppConfig { - } ----- +include-code::./SchedulingConfiguration[tag=snippet,indent=0] You can pick and choose the relevant annotations for your application. For example, if you need only support for `@Scheduled`, you can omit `@EnableAsync`. For more fine-grained control, you can additionally implement the `SchedulingConfigurer` interface, the `AsyncConfigurer` interface, or both. See the -{api-spring-framework}/scheduling/annotation/SchedulingConfigurer.html[`SchedulingConfigurer`] -and {api-spring-framework}/scheduling/annotation/AsyncConfigurer.html[`AsyncConfigurer`] +{spring-framework-api}/scheduling/annotation/SchedulingConfigurer.html[`SchedulingConfigurer`] +and {spring-framework-api}/scheduling/annotation/AsyncConfigurer.html[`AsyncConfigurer`] javadoc for full details. -If you prefer XML configuration, you can use the `` element, -as the following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - ----- - Note that, with the preceding XML, an executor reference is provided for handling those tasks that correspond to methods with the `@Async` annotation, and the scheduler reference is provided for managing those methods annotated with `@Scheduled`. @@ -477,12 +437,12 @@ the framework to invoke a suspending function as a `Publisher`. The Spring Framework will obtain a `Publisher` for the annotated method once and will schedule a `Runnable` in which it subscribes to said `Publisher`. These inner regular -subscriptions occur according to the corresponding `cron`/fixedDelay`/`fixedRate` configuration. +subscriptions occur according to the corresponding `cron`/`fixedDelay`/`fixedRate` configuration. If the `Publisher` emits `onNext` signal(s), these are ignored and discarded (the same way return values from synchronous `@Scheduled` methods are ignored). -In the following example, the `Flux` emits `onNext("Hello"), onNext("World")` every 5 +In the following example, the `Flux` emits `onNext("Hello")`, `onNext("World")` every 5 seconds, but these values are unused: [source,java,indent=0,subs="verbatim,quotes"] @@ -523,7 +483,7 @@ seconds: ==== When destroying the annotated bean or closing the application context, Spring Framework cancels scheduled tasks, which includes the next scheduled subscription to the `Publisher` as well -as any past subscription that is still currently active (e.g. for long-running publishers +as any past subscription that is still currently active (for example, for long-running publishers or even infinite publishers). ==== @@ -673,7 +633,7 @@ scheduled with a trigger. [[scheduling-task-namespace-scheduler]] -=== The 'scheduler' Element +=== The `scheduler` Element The following element creates a `ThreadPoolTaskScheduler` instance with the specified thread pool size: @@ -722,7 +682,7 @@ In the preceding configuration, a `queue-capacity` value has also been provided. The configuration of the thread pool should also be considered in light of the executor's queue capacity. For the full description of the relationship between pool size and queue capacity, see the documentation for -https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html[`ThreadPoolExecutor`]. +{java-api}/java.base/java/util/concurrent/ThreadPoolExecutor.html[`ThreadPoolExecutor`]. The main idea is that, when a task is submitted, the executor first tries to use a free thread if the number of active threads is currently less than the core size. If the core size has been reached, the task is added to the queue, as long as its @@ -731,7 +691,7 @@ reached, does the executor create a new thread beyond the core size. If the max has also been reached, then the executor rejects the task. By default, the queue is unbounded, but this is rarely the desired configuration, -because it can lead to `OutOfMemoryErrors` if enough tasks are added to that queue while +because it can lead to `OutOfMemoryError` if enough tasks are added to that queue while all pool threads are busy. Furthermore, if the queue is unbounded, the max size has no effect at all. Since the executor always tries the queue before creating a new thread beyond the core size, a queue must have a finite capacity for the thread pool to @@ -784,7 +744,7 @@ The following example sets the `keep-alive` value to two minutes: [[scheduling-task-namespace-scheduled-tasks]] -=== The 'scheduled-tasks' Element +=== The `scheduled-tasks` Element The most powerful feature of Spring's task namespace is the support for configuring tasks to be scheduled within a Spring Application Context. This follows an approach @@ -1093,7 +1053,7 @@ we need to set up the `SchedulerFactoryBean`, as the following example shows: More properties are available for the `SchedulerFactoryBean`, such as the calendars used by the job details, properties to customize Quartz with, and a Spring-provided JDBC DataSource. See -the {api-spring-framework}/scheduling/quartz/SchedulerFactoryBean.html[`SchedulerFactoryBean`] +the {spring-framework-api}/scheduling/quartz/SchedulerFactoryBean.html[`SchedulerFactoryBean`] javadoc for more information. NOTE: `SchedulerFactoryBean` also recognizes a `quartz.properties` file in the classpath, diff --git a/framework-docs/modules/ROOT/pages/languages/dynamic.adoc b/framework-docs/modules/ROOT/pages/languages/dynamic.adoc index fed4d8574e70..c0b5ccec7ca1 100644 --- a/framework-docs/modules/ROOT/pages/languages/dynamic.adoc +++ b/framework-docs/modules/ROOT/pages/languages/dynamic.adoc @@ -10,7 +10,7 @@ objects. Spring's scripting support primarily targets Groovy and BeanShell. Beyond those specifically supported languages, the JSR-223 scripting mechanism is supported for integration with any JSR-223 capable language provider (as of Spring 4.2), -e.g. JRuby. +for example, JRuby. You can find fully working examples of where this dynamic language support can be immediately useful in xref:languages/dynamic.adoc#dynamic-language-scenarios[Scenarios]. @@ -179,7 +179,7 @@ Each of the supported languages has a corresponding `` element: * `` (Groovy) * `` (BeanShell) -* `` (JSR-223, e.g. with JRuby) +* `` (JSR-223, for example, with JRuby) The exact attributes and child elements that are available for configuration depends on exactly which language the bean has been defined in (the language-specific sections diff --git a/framework-docs/modules/ROOT/pages/languages/groovy.adoc b/framework-docs/modules/ROOT/pages/languages/groovy.adoc index 745101a99374..e50f136b23bf 100644 --- a/framework-docs/modules/ROOT/pages/languages/groovy.adoc +++ b/framework-docs/modules/ROOT/pages/languages/groovy.adoc @@ -8,7 +8,7 @@ existing Java application. The Spring Framework provides a dedicated `ApplicationContext` that supports a Groovy-based Bean Definition DSL. For more details, see -xref:core/beans/basics.adoc#groovy-bean-definition-dsl[The Groovy Bean Definition DSL]. +xref:core/beans/basics.adoc#beans-factory-groovy[The Groovy Bean Definition DSL]. Further support for Groovy, including beans written in Groovy, refreshable script beans, and more is available in xref:languages/dynamic.adoc[Dynamic Language Support]. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin.adoc index c6beb7f4a6ca..41fbd6bb4f67 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin.adoc @@ -2,9 +2,9 @@ = Kotlin :page-section-summary-toc: 1 -https://kotlinlang.org[Kotlin] is a statically typed language that targets the JVM +{kotlin-site}[Kotlin] is a statically typed language that targets the JVM (and other platforms) which allows writing concise and elegant code while providing -very good https://kotlinlang.org/docs/reference/java-interop.html[interoperability] +very good {kotlin-docs}/java-interop.html[interoperability] with existing libraries written in Java. The Spring Framework provides first-class support for Kotlin and lets developers write @@ -13,13 +13,13 @@ Most of the code samples of the reference documentation are provided in Kotlin in addition to Java. The easiest way to build a Spring application with Kotlin is to leverage Spring Boot and -its https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html[dedicated Kotlin support]. -https://spring.io/guides/tutorials/spring-boot-kotlin/[This comprehensive tutorial] +its {spring-boot-docs-ref}/features/kotlin.html[dedicated Kotlin support]. +{spring-site-guides}/tutorials/spring-boot-kotlin/[This comprehensive tutorial] will teach you how to build Spring Boot applications with Kotlin using https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io]. Feel free to join the #spring channel of https://slack.kotlinlang.org/[Kotlin Slack] or ask a question with `spring` and `kotlin` as tags on -https://stackoverflow.com/questions/tagged/spring+kotlin[Stackoverflow] if you need support. +{stackoverflow-spring-kotlin-tags}[Stackoverflow] if you need support. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc index 4a724caf0115..1725a5dc98f2 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc @@ -1,7 +1,7 @@ [[kotlin-annotations]] = Annotations -The Spring Framework also takes advantage of https://kotlinlang.org/docs/reference/null-safety.html[Kotlin null-safety] +The Spring Framework also takes advantage of {kotlin-docs}/null-safety.html[Kotlin null-safety] to determine if an HTTP parameter is required without having to explicitly define the `required` attribute. That means `@RequestParam name: String?` is treated as not required and, conversely, `@RequestParam name: String` is treated as being required. @@ -14,16 +14,13 @@ For example, `@Autowired lateinit var thing: Thing` implies that a bean of type `Thing` must be registered in the application context, while `@Autowired lateinit var thing: Thing?` does not raise an error if such a bean does not exist. -Following the same principle, `@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)` implies +Following the same principle, `@Bean fun play(toy: Toy, car: Car?) = Baz(toy, car)` implies that a bean of type `Toy` must be registered in the application context, while a bean of type `Car` may or may not exist. The same behavior applies to autowired constructor parameters. NOTE: If you use bean validation on classes with properties or a primary constructor -parameters, you may need to use -https://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets[annotation use-site targets], +with parameters, you may need to use +{kotlin-docs}/annotations.html#annotation-use-site-targets[annotation use-site targets], such as `@field:NotNull` or `@get:Size(min=5, max=15)`, as described in -https://stackoverflow.com/a/35853200/1092077[this Stack Overflow response]. - - - +{stackoverflow-site}/a/35853200/1092077[this Stack Overflow response]. 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 68949a9bf0bc..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 {docs-spring-framework}/kdoc-api/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 -https://github.com/spring-projects/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 https://stackoverflow.com/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 https://github.com/spring-projects/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/classes-interfaces.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/classes-interfaces.adoc index 5dd4520dd9f5..604563704a50 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/classes-interfaces.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/classes-interfaces.adoc @@ -12,7 +12,7 @@ compiler flag to be enabled during compilation. (For completeness, we neverthele running the Kotlin compiler with its `-java-parameters` flag for standard Java parameter exposure.) You can declare configuration classes as -https://kotlinlang.org/docs/reference/nested-classes.html[top level or nested but not inner], +{kotlin-docs}/nested-classes.html[top level or nested but not inner], since the later requires a reference to the outer class. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc index 17becb7aa151..9e09a69411ab 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc @@ -1,21 +1,23 @@ [[coroutines]] = Coroutines -Kotlin https://kotlinlang.org/docs/reference/coroutines-overview.html[Coroutines] are Kotlin +Kotlin {kotlin-docs}/coroutines-overview.html[Coroutines] are Kotlin lightweight threads allowing to write non-blocking code in an imperative way. On language side, suspending functions provides an abstraction for asynchronous operations while on library side -https://github.com/Kotlin/kotlinx.coroutines[kotlinx.coroutines] provides functions like -https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html[`async { }`] -and types like https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[`Flow`]. +{kotlin-github-org}/kotlinx.coroutines[kotlinx.coroutines] provides functions like +{kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines/async.html[`async { }`] +and types like {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[`Flow`]. Spring Framework provides support for Coroutines on the following scope: -* https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html[Deferred] and https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[Flow] return values support in Spring MVC and WebFlux annotated `@Controller` +* {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html[Deferred] and {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[Flow] return values support in Spring MVC and WebFlux annotated `@Controller` * Suspending function support in Spring MVC and WebFlux annotated `@Controller` -* Extensions for WebFlux {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.client/index.html[client] and {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/index.html[server] functional API. -* WebFlux.fn {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL +* Extensions for WebFlux {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.client/index.html[client] and {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/index.html[server] functional API. +* WebFlux.fn {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL +* WebFlux {spring-framework-api-kdoc}/spring-web/org.springframework.web.server/-co-web-filter/index.html[`CoWebFilter`] * Suspending function and `Flow` support in RSocket `@MessageMapping` annotated methods -* Extensions for {docs-spring-framework}/kdoc-api/spring-messaging/org.springframework.messaging.rsocket/index.html[`RSocketRequester`] +* Extensions for {spring-framework-api-kdoc}/spring-messaging/org.springframework.messaging.rsocket/index.html[`RSocketRequester`] +* Spring AOP @@ -53,17 +55,17 @@ For input parameters: * If laziness is not needed, `fun handler(mono: Mono)` becomes `fun handler(value: T)` since a suspending functions can be invoked to get the value parameter. * If laziness is needed, `fun handler(mono: Mono)` becomes `fun handler(supplier: suspend () -> T)` or `fun handler(supplier: suspend () -> T?)` -https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[`Flow`] is `Flux` equivalent in Coroutines world, suitable for hot or cold stream, finite or infinite streams, with the following main differences: +{kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html[`Flow`] is `Flux` equivalent in Coroutines world, suitable for hot or cold stream, finite or infinite streams, with the following main differences: * `Flow` is push-based while `Flux` is push-pull hybrid * Backpressure is implemented via suspending functions -* `Flow` has only a https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/collect.html[single suspending `collect` method] and operators are implemented as https://kotlinlang.org/docs/reference/extensions.html[extensions] -* https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-core/common/src/flow/operators[Operators are easy to implement] thanks to Coroutines +* `Flow` has only a {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/collect.html[single suspending `collect` method] and operators are implemented as {kotlin-docs}/extensions.html[extensions] +* {kotlin-github-org}/kotlinx.coroutines/tree/master/kotlinx-coroutines-core/common/src/flow/operators[Operators are easy to implement] thanks to Coroutines * Extensions allow to add custom operators to `Flow` * Collect operations are suspending functions -* https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html[`map` operator] supports asynchronous operation (no need for `flatMap`) since it takes a suspending function parameter +* {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html[`map` operator] supports asynchronous operation (no need for `flatMap`) since it takes a suspending function parameter -Read this blog post about https://spring.io/blog/2019/04/12/going-reactive-with-spring-coroutines-and-kotlin-flow[Going Reactive with Spring, Coroutines and Kotlin Flow] +Read this blog post about {spring-site-blog}/2019/04/12/going-reactive-with-spring-coroutines-and-kotlin-flow[Going Reactive with Spring, Coroutines and Kotlin Flow] for more details, including how to run code concurrently with Coroutines. @@ -170,7 +172,7 @@ class CoroutinesViewController(banner: Banner) { [[webflux-fn]] == WebFlux.fn -Here is an example of Coroutines router defined via the {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL and related handlers. +Here is an example of Coroutines router defined via the {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] DSL and related handlers. [source,kotlin,indent=0] ---- @@ -207,7 +209,7 @@ class UserHandler(builder: WebClient.Builder) { == Transactions Transactions on Coroutines are supported via the programmatic variant of the Reactive -transaction management provided as of Spring Framework 5.2. +transaction management. For suspending functions, a `TransactionalOperator.executeAndAwait` extension is provided. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc index d7da3aee5328..6af9b086ae9f 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc @@ -1,11 +1,11 @@ [[kotlin-extensions]] = Extensions -Kotlin https://kotlinlang.org/docs/reference/extensions.html[extensions] provide the ability +Kotlin {kotlin-docs}/extensions.html[extensions] provide the ability to extend existing classes with additional functionality. The Spring Framework Kotlin APIs use these extensions to add new Kotlin-specific conveniences to existing Spring APIs. -The {docs-spring-framework}/kdoc-api/[Spring Framework KDoc API] lists +The {spring-framework-api-kdoc}/[Spring Framework KDoc API] lists and documents all available Kotlin extensions and DSLs. NOTE: Keep in mind that Kotlin extensions need to be imported to be used. This means, @@ -13,8 +13,8 @@ for example, that the `GenericApplicationContext.registerBean` Kotlin extension is available only if `org.springframework.context.support.registerBean` is imported. That said, similar to static imports, an IDE should automatically suggest the import in most cases. -For example, https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters[Kotlin reified type parameters] -provide a workaround for JVM https://docs.oracle.com/javase/tutorial/java/generics/erasure.html[generics type erasure], +For example, {kotlin-docs}/inline-functions.html#reified-type-parameters[Kotlin reified type parameters] +provide a workaround for JVM {java-tutorial}/java/generics/erasure.html[generics type erasure], and the Spring Framework provides some extensions to take advantage of this feature. This allows for a better Kotlin API `RestTemplate`, for the new `WebClient` from Spring WebFlux, and for various other APIs. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc index c53a37351ded..6b8b75b491ec 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc @@ -2,7 +2,7 @@ = Getting Started The easiest way to learn how to build a Spring application with Kotlin is to follow -https://spring.io/guides/tutorials/spring-boot-kotlin/[the dedicated tutorial]. +{spring-site-guides}/tutorials/spring-boot-kotlin/[the dedicated tutorial]. @@ -10,22 +10,21 @@ https://spring.io/guides/tutorials/spring-boot-kotlin/[the dedicated tutorial]. == `start.spring.io` The easiest way to start a new Spring Framework project in Kotlin is to create a new Spring -Boot 2 project on https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io]. +Boot project on https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin[start.spring.io]. [[choosing-the-web-flavor]] == Choosing the Web Flavor -Spring Framework now comes with two different web stacks: xref:web/webmvc.adoc#mvc[Spring MVC] and +Spring Framework comes with two different web stacks: xref:web/webmvc.adoc#mvc[Spring MVC] and xref:testing/unit.adoc#mock-objects-web-reactive[Spring WebFlux]. Spring WebFlux is recommended if you want to create applications that will deal with latency, -long-lived connections, streaming scenarios or if you want to use the web functional -Kotlin DSL. +long-lived connections or streaming scenarios. For other use cases, especially if you are using blocking technologies such as JPA, Spring -MVC and its annotation-based programming model is the recommended choice. +MVC is the recommended choice. 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 dc1a3f0257b8..213a04c9dfa6 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc @@ -1,38 +1,15 @@ [[kotlin-null-safety]] = Null-safety -One of Kotlin's key features is https://kotlinlang.org/docs/reference/null-safety.html[null-safety], +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 -https://www.baeldung.com/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 -https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[platform types], -for which null-checks are relaxed. -https://kotlinlang.org/docs/reference/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 https://github.com/Kotlin/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 80a2b48fc15a..fcc26798b546 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc @@ -2,16 +2,13 @@ = Requirements :page-section-summary-toc: 1 -Spring Framework supports Kotlin 1.3+ and requires +Spring Framework supports Kotlin 2.1+ and requires https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib[`kotlin-stdlib`] -(or one of its variants, such as https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8[`kotlin-stdlib-jdk8`]) 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 https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io]. -WARNING: Kotlin https://kotlinlang.org/docs/inline-classes.html[inline classes] are not yet supported. - -NOTE: The https://github.com/FasterXML/jackson-module-kotlin[Jackson Kotlin module] is required +NOTE: The {jackson-github-org}/jackson-module-kotlin[Jackson Kotlin module] is required for serializing or deserializing JSON data for Kotlin classes with Jackson, so make sure to add the `com.fasterxml.jackson.module:jackson-module-kotlin` dependency to your project if you have such need. It is automatically registered when found in the classpath. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc index a99666581818..f3be082c275a 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc @@ -4,9 +4,9 @@ We recommend the following resources for people learning how to build applications with Kotlin and the Spring Framework: -* https://kotlinlang.org/docs/reference/[Kotlin language reference] +* {kotlin-docs}[Kotlin language reference] * https://slack.kotlinlang.org/[Kotlin Slack] (with a dedicated #spring channel) -* https://stackoverflow.com/questions/tagged/spring+kotlin[Stackoverflow, with `spring` and `kotlin` tags] +* {stackoverflow-spring-kotlin-tags}[Stackoverflow, with `spring` and `kotlin` tags] * https://play.kotlinlang.org/[Try Kotlin in your browser] * https://blog.jetbrains.com/kotlin/[Kotlin blog] * https://kotlin.link/[Awesome Kotlin] @@ -18,28 +18,9 @@ Kotlin and the Spring Framework: The following Github projects offer examples that you can learn from and possibly even extend: +* https://github.com/spring-guides/tut-spring-boot-kotlin[tut-spring-boot-kotlin]: Sources of {spring-site}/guides/tutorials/spring-boot-kotlin/[the official Spring + Kotlin tutorial] * https://github.com/sdeleuze/spring-boot-kotlin-demo[spring-boot-kotlin-demo]: Regular Spring Boot and Spring Data JPA project -* https://github.com/mixitconf/mixit[mixit]: Spring Boot 2, WebFlux, and Reactive Spring Data MongoDB +* https://github.com/mixitconf/mixit[mixit]: Spring Boot, WebFlux, and Reactive Spring Data MongoDB * https://github.com/sdeleuze/spring-kotlin-functional[spring-kotlin-functional]: Standalone WebFlux and functional bean definition DSL * https://github.com/sdeleuze/spring-kotlin-fullstack[spring-kotlin-fullstack]: WebFlux Kotlin fullstack example with Kotlin2js for frontend instead of JavaScript or TypeScript * https://github.com/spring-petclinic/spring-petclinic-kotlin[spring-petclinic-kotlin]: Kotlin version of the Spring PetClinic Sample Application -* https://github.com/sdeleuze/spring-kotlin-deepdive[spring-kotlin-deepdive]: A step-by-step migration guide for Boot 1.0 and Java to Boot 2.0 and Kotlin -* https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-kotlin-samples/spring-cloud-gcp-kotlin-app-sample[spring-cloud-gcp-kotlin-app-sample]: Spring Boot with Google Cloud Platform Integrations - - - -[[issues]] -== Issues - -The following list categorizes the pending issues related to Spring and Kotlin support: - -* Spring Framework -** https://github.com/spring-projects/spring-framework/issues/20606[Unable to use WebTestClient with mock server in Kotlin] -** https://github.com/spring-projects/spring-framework/issues/20496[Support null-safety at generics, varargs and array elements level] -* Kotlin -** https://youtrack.jetbrains.com/issue/KT-6380[Parent issue for Spring Framework support] -** https://youtrack.jetbrains.com/issue/KT-5464[Kotlin requires type inference where Java doesn't] -** https://youtrack.jetbrains.com/issue/KT-20283[Smart cast regression with open classes] -** https://youtrack.jetbrains.com/issue/KT-14984[Impossible to pass not all SAM argument as function] -** https://youtrack.jetbrains.com/issue/KT-15125[Support JSR 223 bindings directly via script variables] -** https://youtrack.jetbrains.com/issue/KT-6653[Kotlin properties do not override Java-style getters and setters] 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 4118859a1585..64da5a0b63ed 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 @@ -9,7 +9,7 @@ in Kotlin. [[final-by-default]] == Final by Default -By default, https://discuss.kotlinlang.org/t/classes-final-by-default/166[all classes in Kotlin are `final`]. +By default, https://discuss.kotlinlang.org/t/classes-final-by-default/166[all classes and member functions in Kotlin are `final`]. The `open` modifier on a class is the opposite of Java's `final`: It allows others to inherit from this class. This also applies to member functions, in that they need to be marked as `open` to be overridden. @@ -21,10 +21,10 @@ member function of Spring beans that are proxied by CGLIB, which can quickly become painful and is against the Kotlin principle of keeping code concise and predictable. NOTE: It is also possible to avoid CGLIB proxies for configuration classes by using `@Configuration(proxyBeanMethods = false)`. -See {api-spring-framework}/context/annotation/Configuration.html#proxyBeanMethods--[`proxyBeanMethods` Javadoc] for more details. +See {spring-framework-api}/context/annotation/Configuration.html#proxyBeanMethods--[`proxyBeanMethods` Javadoc] for more details. Fortunately, Kotlin provides a -https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin[`kotlin-spring`] +{kotlin-docs}/compiler-plugins.html#kotlin-spring-compiler-plugin[`kotlin-spring`] plugin (a preconfigured version of the `kotlin-allopen` plugin) that automatically opens classes and their member functions for types that are annotated or meta-annotated with one of the following annotations: @@ -38,6 +38,12 @@ Meta-annotation support means that types annotated with `@Configuration`, `@Cont `@RestController`, `@Service`, or `@Repository` are automatically opened since these annotations are meta-annotated with `@Component`. +WARNING: Some use cases involving proxies and automatic generation of final methods by the Kotlin compiler require extra +care. For example, a Kotlin class with properties will generate related `final` getters and setters. In order +to be able to proxy related methods, a type level `@Component` annotation should be preferred to method level `@Bean` in +order to have those methods opened by the `kotlin-spring` plugin. A typical use case is `@Scope` and its popular +`@RequestScope` specialization. + https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io] enables the `kotlin-spring` plugin by default. So, in practice, you can write your Kotlin beans without any additional `open` keyword, as in Java. @@ -59,7 +65,7 @@ within the primary constructor, as in the following example: class Person(val name: String, val age: Int) ---- -You can optionally add https://kotlinlang.org/docs/reference/data-classes.html[the `data` keyword] +You can optionally add {kotlin-docs}/data-classes.html[the `data` keyword] to make the compiler automatically derive the following members from all properties declared in the primary constructor: @@ -80,12 +86,12 @@ As the following example shows, this allows for easy changes to individual prope Common persistence technologies (such as JPA) require a default constructor, preventing this kind of design. Fortunately, there is a workaround for this -https://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell["`default constructor hell`"], -since Kotlin provides a https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-jpa-compiler-plugin[`kotlin-jpa`] +{stackoverflow-questions}/32038177/kotlin-with-jpa-default-constructor-hell["`default constructor hell`"], +since Kotlin provides a {kotlin-docs}/compiler-plugins.html#kotlin-jpa-compiler-plugin[`kotlin-jpa`] plugin that generates synthetic no-arg constructor for classes annotated with JPA annotations. If you need to leverage this kind of mechanism for other persistence technologies, you can configure -the https://kotlinlang.org/docs/reference/compiler-plugins.html#how-to-use-no-arg-plugin[`kotlin-noarg`] +the {kotlin-docs}/compiler-plugins.html#how-to-use-no-arg-plugin[`kotlin-noarg`] plugin. NOTE: As of the Kay release train, Spring Data supports Kotlin immutable class instances and @@ -97,8 +103,11 @@ does not require the `kotlin-noarg` plugin if the module uses Spring Data object [[injecting-dependencies]] == Injecting Dependencies +[[favor-constructor-injection]] +=== Favor constructor injection + Our recommendation is to try to favor constructor injection with `val` read-only (and -non-nullable when possible) https://kotlinlang.org/docs/reference/properties.html[properties], +non-nullable when possible) {kotlin-docs}/properties.html[properties], as the following example shows: [source,kotlin,indent=0] @@ -130,20 +139,54 @@ as the following example shows: } ---- +[[internal-functions-name-mangling]] +=== Internal functions name mangling + +Kotlin functions with the `internal` {kotlin-docs}/visibility-modifiers.html#class-members[visibility modifier] have +their names mangled when compiled to JVM bytecode, which has a side effect when injecting dependencies by name. + +For example, this Kotlin class: +[source,kotlin,indent=0] +---- +@Configuration +class SampleConfiguration { + + @Bean + internal fun sampleBean() = SampleBean() +} +---- + +Translates to this Java representation of the compiled JVM bytecode: +[source,java,indent=0] +---- +@Configuration +@Metadata(/* ... */) +public class SampleConfiguration { + + @Bean + @NotNull + public SampleBean sampleBean$demo_kotlin_internal_test() { + return new SampleBean(); + } +} +---- +As a consequence, the related bean name represented as a Kotlin string is `"sampleBean\$demo_kotlin_internal_test"`, +instead of `"sampleBean"` for the regular `public` function use-case. Make sure to use the mangled name when injecting +such bean by name, or add `@JvmName("sampleBean")` to disable name mangling. [[injecting-configuration-properties]] == Injecting Configuration Properties In Java, you can inject configuration properties by using annotations (such as pass:q[`@Value("${property}")`)]. However, in Kotlin, `$` is a reserved character that is used for -https://kotlinlang.org/docs/reference/idioms.html#string-interpolation[string interpolation]. +{kotlin-docs}/idioms.html#string-interpolation[string interpolation]. Therefore, if you wish to use the `@Value` annotation in Kotlin, you need to escape the `$` character by writing pass:q[`@Value("\${property}")`]. NOTE: If you use Spring Boot, you should probably use -https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties[`@ConfigurationProperties`] +{spring-boot-docs-ref}/features/external-config.html#features.external-config.typesafe-configuration-properties[`@ConfigurationProperties`] instead of `@Value` annotations. As an alternative, you can customize the property placeholder prefix by declaring the @@ -177,14 +220,14 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl [[checked-exceptions]] == Checked Exceptions -Java and https://kotlinlang.org/docs/reference/exceptions.html[Kotlin exception handling] +Java and {kotlin-docs}/exceptions.html[Kotlin exception handling] are pretty close, with the main difference being that Kotlin treats all exceptions as unchecked exceptions. However, when using proxied objects (for example classes or methods annotated with `@Transactional`), checked exceptions thrown will be wrapped by default in an `UndeclaredThrowableException`. To get the original exception thrown like in Java, methods should be annotated with -https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-throws/index.html[`@Throws`] +{kotlin-api}/jvm/stdlib/kotlin.jvm/-throws/index.html[`@Throws`] to specify explicitly the checked exceptions thrown (for example `@Throws(IOException::class)`). @@ -194,7 +237,7 @@ to specify explicitly the checked exceptions thrown (for example `@Throws(IOExce Kotlin annotations are mostly similar to Java annotations, but array attributes (which are extensively used in Spring) behave differently. As explained in the -https://kotlinlang.org/docs/reference/annotations.html[Kotlin documentation] you can omit +{kotlin-docs}/annotations.html[Kotlin documentation] you can omit the `value` attribute name, unlike other attributes, and specify it as a `vararg` parameter. To understand what that means, consider `@RequestMapping` (which is one of the most widely @@ -240,13 +283,13 @@ be matched, not only the `GET` method. == Declaration-site variance Dealing with generic types in Spring applications written in Kotlin may require, for some use cases, to understand -Kotlin https://kotlinlang.org/docs/generics.html#declaration-site-variance[declaration-site variance] +Kotlin {kotlin-docs}/generics.html#declaration-site-variance[declaration-site variance] which allows to define the variance when declaring a type, which is not possible in Java which supports only use-site variance. For example, declaring `List` in Kotlin is conceptually equivalent to `java.util.List` because `kotlin.collections.List` is declared as -https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/[`interface List : kotlin.collections.Collection`]. +{kotlin-api}/jvm/stdlib/kotlin.collections/-list/[`interface List : kotlin.collections.Collection`]. This needs to be taken into account by using the `out` Kotlin keyword on generic types when using Java classes, for example when writing a `org.springframework.core.convert.converter.Converter` from a Kotlin type to a Java type. @@ -267,7 +310,7 @@ class ListOfAnyConverter : Converter, CustomJavaList<*>> { ---- NOTE: Spring Framework does not leverage yet declaration-site variance type information for injecting beans, -subscribe to https://github.com/spring-projects/spring-framework/issues/22313[spring-framework#22313] to track related +subscribe to {spring-framework-issues}/22313[spring-framework#22313] to track related progresses. @@ -280,7 +323,7 @@ The recommended testing framework is https://junit.org/junit5/[JUnit 5] along wi https://mockk.io/[Mockk] for mocking. NOTE: If you are using Spring Boot, see -https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-kotlin-testing[this related documentation]. +{spring-boot-docs-ref}/features/kotlin.html#features.kotlin.testing[this related documentation]. [[constructor-injection]] @@ -289,7 +332,7 @@ https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-featu 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 in order to use `val` instead of `lateinit var`. You can use -{api-spring-framework}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`] +{spring-framework-api}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`] to enable autowiring for all parameters. NOTE: You can also change the default behavior to `ALL` in a `junit-platform.properties` @@ -383,15 +426,3 @@ class SpecificationLikeTests { ---- -[[kotlin-webtestclient-issue]] -=== `WebTestClient` Type Inference Issue in Kotlin - -Due to a https://youtrack.jetbrains.com/issue/KT-5464[type inference issue], you must -use the Kotlin `expectBody` extension (such as `.expectBody().isEqualTo("toys")`), -since it provides a workaround for the Kotlin issue with the Java API. - -See also the related https://jira.spring.io/browse/SPR-16057[SPR-16057] issue. - - - - diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc index 0b44ce3c7432..0b4a6e555c3a 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc @@ -8,9 +8,9 @@ Spring Framework comes with a Kotlin router DSL available in 3 flavors: -* WebMvc.fn DSL with {docs-spring-framework}/kdoc-api/spring-webmvc/org.springframework.web.servlet.function/router.html[router { }] -* WebFlux.fn <> DSL with {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/router.html[router { }] -* WebFlux.fn <> DSL with {docs-spring-framework}/kdoc-api/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] +* xref:web/webmvc-functional.adoc[WebMvc.fn DSL] with {spring-framework-api-kdoc}/spring-webmvc/org.springframework.web.servlet.function/router.html[router { }] +* xref:web/webflux-functional.adoc[WebFlux.fn Reactive DSL] with {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/router.html[router { }] +* xref:languages/kotlin/coroutines.adoc[WebFlux.fn Coroutines DSL] with {spring-framework-api-kdoc}/spring-webflux/org.springframework.web.reactive.function.server/co-router.html[coRouter { }] These DSL let you write clean and idiomatic Kotlin code to build a `RouterFunction` instance as the following example shows: @@ -75,66 +75,13 @@ mockMvc.get("/person/{name}", "Lee") { -[[kotlin-script-templates]] -== Kotlin Script Templates - -Spring Framework provides a -https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html[`ScriptTemplateView`] -which supports https://www.jcp.org/en/jsr/detail?id=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 -https://github.com/Kotlin/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 - } - - @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 -As of Spring Framework 5.3, https://github.com/Kotlin/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 https://github.com/Kotlin/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. - - - +{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 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/overview.adoc b/framework-docs/modules/ROOT/pages/overview.adoc index b0ed76a7d869..da80bf35094d 100644 --- a/framework-docs/modules/ROOT/pages/overview.adoc +++ b/framework-docs/modules/ROOT/pages/overview.adoc @@ -10,7 +10,7 @@ Spring requires Java 17+. Spring supports a wide range of application scenarios. In a large enterprise, applications often exist for a long time and have to run on a JDK and application server whose upgrade -cycle is beyond developer control. Others may run as a single jar with the server embedded, +cycle is beyond the developer's control. Others may run as a single jar with the server embedded, possibly in a cloud environment. Yet others may be standalone applications (such as batch or integration workloads) that do not need a server. @@ -37,12 +37,12 @@ support for different application architectures, including messaging, transactio persistence, and web. It also includes the Servlet-based Spring MVC web framework and, in parallel, the Spring WebFlux reactive web framework. -A note about modules: Spring's framework jars allow for deployment to JDK 9's module path -("Jigsaw"). For use in Jigsaw-enabled applications, the Spring Framework 5 jars come with -"Automatic-Module-Name" manifest entries which define stable language-level module names -("spring.core", "spring.context", etc.) independent from jar artifact names (the jars follow -the same naming pattern with "-" instead of ".", e.g. "spring-core" and "spring-context"). -Of course, Spring's framework jars keep working fine on the classpath on both JDK 8 and 9+. +A note about modules: Spring Framework's jars allow for deployment to the module path (Java +Module System). For use in module-enabled applications, the Spring Framework jars come with +`Automatic-Module-Name` manifest entries which define stable language-level module names +(`spring.core`, `spring.context`, etc.) independent from jar artifact names. The jars follow +the same naming pattern with `-` instead of `.` – for example, `spring-core` and `spring-context`. +Of course, Spring Framework's jars also work fine on the classpath. @@ -57,23 +57,23 @@ competition with Spring, they are in fact complementary. The Spring programming model does not embrace the Jakarta EE platform specification; rather, it integrates with carefully selected individual specifications from the traditional EE umbrella: -* Servlet API (https://jcp.org/en/jsr/detail?id=340[JSR 340]) -* WebSocket API (https://www.jcp.org/en/jsr/detail?id=356[JSR 356]) -* Concurrency Utilities (https://www.jcp.org/en/jsr/detail?id=236[JSR 236]) -* JSON Binding API (https://jcp.org/en/jsr/detail?id=367[JSR 367]) -* Bean Validation (https://jcp.org/en/jsr/detail?id=303[JSR 303]) -* JPA (https://jcp.org/en/jsr/detail?id=338[JSR 338]) -* JMS (https://jcp.org/en/jsr/detail?id=914[JSR 914]) +* Servlet API ({JSR}340[JSR 340]) +* WebSocket API ({JSR}356[JSR 356]) +* Concurrency Utilities ({JSR}236[JSR 236]) +* JSON Binding API ({JSR}367[JSR 367]) +* Bean Validation ({JSR}303[JSR 303]) +* JPA ({JSR}338[JSR 338]) +* JMS ({JSR}914[JSR 914]) * as well as JTA/JCA setups for transaction coordination, if necessary. The Spring Framework also supports the Dependency Injection -(https://www.jcp.org/en/jsr/detail?id=330[JSR 330]) and Common Annotations -(https://jcp.org/en/jsr/detail?id=250[JSR 250]) specifications, which application +({JSR}330[JSR 330]) and Common Annotations +({JSR}250[JSR 250]) specifications, which application developers may choose to use instead of the Spring-specific mechanisms provided by the Spring Framework. Originally, those were based on common `javax` packages. As of Spring Framework 6.0, Spring has been upgraded to the Jakarta EE 9 level -(e.g. Servlet 5.0+, JPA 3.0+), based on the `jakarta` namespace instead of the +(for example, Servlet 5.0+, JPA 3.0+), based on the `jakarta` namespace instead of the traditional `javax` packages. With EE 9 as the minimum and EE 10 supported already, Spring is prepared to provide out-of-the-box support for the further evolution of the Jakarta EE APIs. Spring Framework 6.0 is fully compatible with Tomcat 10.1, @@ -89,7 +89,7 @@ and can run on servers (such as Netty) that are not Servlet containers. Spring continues to innovate and to evolve. Beyond the Spring Framework, there are other projects, such as Spring Boot, Spring Security, Spring Data, Spring Cloud, Spring Batch, among others. It’s important to remember that each project has its own source code repository, -issue tracker, and release cadence. See https://spring.io/projects[spring.io/projects] for +issue tracker, and release cadence. See {spring-site-projects}[spring.io/projects] for the complete list of Spring projects. @@ -125,17 +125,17 @@ clean code structure with no circular dependencies between packages. == Feedback and Contributions For how-to questions or diagnosing or debugging issues, we suggest using Stack Overflow. Click -https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-r2dbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux+or+spring-rsocket?tab=Newest[here] +{stackoverflow-spring-tag}+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-r2dbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux+or+spring-rsocket?tab=Newest[here] for a list of the suggested tags to use on Stack Overflow. If you're fairly certain that there is a problem in the Spring Framework or would like to suggest a feature, please use -the https://github.com/spring-projects/spring-framework/issues[GitHub Issues]. +the {spring-framework-issues}[GitHub Issues]. If you have a solution in mind or a suggested fix, you can submit a pull request on -https://github.com/spring-projects/spring-framework[Github]. However, please keep in mind +{spring-framework-github}[Github]. However, please keep in mind that, for all but the most trivial issues, we expect a ticket to be filed in the issue tracker, where discussions take place and leave a record for future reference. -For more details see the guidelines at the {spring-framework-main-code}/CONTRIBUTING.md[CONTRIBUTING], +For more details see the guidelines at the {spring-framework-code}/CONTRIBUTING.md[CONTRIBUTING], top-level project page. @@ -145,15 +145,15 @@ top-level project page. == Getting Started If you are just getting started with Spring, you may want to begin using the Spring -Framework by creating a https://projects.spring.io/spring-boot/[Spring Boot]-based +Framework by creating a {spring-site-projects}/spring-boot/[Spring Boot]-based application. Spring Boot provides a quick (and opinionated) way to create a production-ready Spring-based application. It is based on the Spring Framework, favors convention over configuration, and is designed to get you up and running as quickly as possible. You can use https://start.spring.io/[start.spring.io] to generate a basic project or follow -one of the https://spring.io/guides["Getting Started" guides], such as -https://spring.io/guides/gs/rest-service/[Getting Started Building a RESTful Web Service]. +one of the {spring-site-guides}["Getting Started" guides], such as +{spring-site-guides}/gs/rest-service/[Getting Started Building a RESTful Web Service]. As well as being easier to digest, these guides are very task focused, and most of them are based on Spring Boot. They also cover other projects from the Spring portfolio that you might want to consider when solving a particular problem. diff --git a/framework-docs/modules/ROOT/pages/rsocket.adoc b/framework-docs/modules/ROOT/pages/rsocket.adoc index 86846ae5cfdd..52e42adf8001 100644 --- a/framework-docs/modules/ROOT/pages/rsocket.adoc +++ b/framework-docs/modules/ROOT/pages/rsocket.adoc @@ -23,7 +23,7 @@ while the above interactions are called "request streams" or simply "requests". These are the key features and benefits of the RSocket protocol: -* https://www.reactive-streams.org/[Reactive Streams] semantics across network boundary -- +* {reactive-streams-site}/[Reactive Streams] semantics across network boundary -- for streaming requests such as `Request-Stream` and `Channel`, back pressure signals travel between requester and responder, allowing a requester to slow down a responder at the source, hence reducing reliance on network layer congestion control, and the need @@ -38,9 +38,9 @@ the amount of state required. * Fragmentation and re-assembly of large messages. * Keepalive (heartbeats). -RSocket has {gh-rsocket}[implementations] in multiple languages. The -{gh-rsocket-java}[Java library] is built on https://projectreactor.io/[Project Reactor], -and https://github.com/reactor/reactor-netty[Reactor Netty] for the transport. That means +RSocket has {rsocket-github-org}[implementations] in multiple languages. The +{rsocket-java}[Java library] is built on {reactor-site}/[Project Reactor], +and {reactor-github-org}/reactor-netty[Reactor Netty] for the transport. That means signals from Reactive Streams Publishers in your application propagate transparently through RSocket across the network. @@ -50,8 +50,8 @@ through RSocket across the network. === The Protocol One of the benefits of RSocket is that it has well defined behavior on the wire and an -easy to read https://rsocket.io/about/protocol[specification] along with some protocol -{gh-rsocket}/rsocket/tree/master/Extensions[extensions]. Therefore it is +easy to read {rsocket-site}/about/protocol[specification] along with some protocol +{rsocket-protocol-extensions}[extensions]. Therefore it is a good idea to read the spec, independent of language implementations and higher level framework APIs. This section provides a succinct overview to establish some context. @@ -96,28 +96,28 @@ and therefore only included in the first message on a request, i.e. with one of Protocol extensions define common metadata formats for use in applications: -* {gh-rsocket-extensions}/CompositeMetadata.md[Composite Metadata]-- multiple, +* {rsocket-protocol-extensions}/CompositeMetadata.md[Composite Metadata]-- multiple, independently formatted metadata entries. -* {gh-rsocket-extensions}/Routing.md[Routing] -- the route for a request. +* {rsocket-protocol-extensions}/Routing.md[Routing] -- the route for a request. [[rsocket-java]] === Java Implementation -The {gh-rsocket-java}[Java implementation] for RSocket is built on -https://projectreactor.io/[Project Reactor]. The transports for TCP and WebSocket are -built on https://github.com/reactor/reactor-netty[Reactor Netty]. As a Reactive Streams +The {rsocket-java}[Java implementation] for RSocket is built on +{reactor-site}/[Project Reactor]. The transports for TCP and WebSocket are +built on {reactor-github-org}/reactor-netty[Reactor Netty]. As a Reactive Streams library, Reactor simplifies the job of implementing the protocol. For applications it is a natural fit to use `Flux` and `Mono` with declarative operators and transparent back pressure support. The API in RSocket Java is intentionally minimal and basic. It focuses on protocol -features and leaves the application programming model (e.g. RPC codegen vs other) as a +features and leaves the application programming model (for example, RPC codegen vs other) as a higher level, independent concern. The main contract -{gh-rsocket-java}/blob/master/rsocket-core/src/main/java/io/rsocket/RSocket.java[io.rsocket.RSocket] +{rsocket-java-code}/rsocket-core/src/main/java/io/rsocket/RSocket.java[io.rsocket.RSocket] models the four request interaction types with `Mono` representing a promise for a single message, `Flux` a stream of messages, and `io.rsocket.Payload` the actual message with access to data and metadata as byte buffers. The `RSocket` contract is used @@ -127,7 +127,7 @@ requests with. For responding, the application implements `RSocket` to handle re This is not meant to be a thorough introduction. For the most part, Spring applications will not have to use its API directly. However it may be important to see or experiment with RSocket independent of Spring. The RSocket Java repository contains a number of -{gh-rsocket-java}/tree/master/rsocket-examples[sample apps] that +{rsocket-java-code}/rsocket-examples[sample apps] that demonstrate its API and protocol features. @@ -152,7 +152,7 @@ Spring Boot 2.2 supports standing up an RSocket server over TCP or WebSocket, in the option to expose RSocket over WebSocket in a WebFlux server. There is also client support and auto-configuration for an `RSocketRequester.Builder` and `RSocketStrategies`. See the -https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-rsocket[RSocket section] +{spring-boot-docs}/messaging.html#messaging.rsocket[RSocket section] in the Spring Boot reference for more details. Spring Security 5.2 provides RSocket support. @@ -186,7 +186,7 @@ This is the most basic way to connect with default settings: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RSocketRequester requester = RSocketRequester.builder().tcp("localhost", 7000); @@ -196,7 +196,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val requester = RSocketRequester.builder().tcp("localhost", 7000) @@ -222,7 +222,7 @@ established transparently and used. For data, the default mime type is derived from the first configured `Decoder`. For metadata, the default mime type is -{gh-rsocket-extensions}/CompositeMetadata.md[composite metadata] which allows multiple +{rsocket-protocol-extensions}/CompositeMetadata.md[composite metadata] which allows multiple metadata value and mime type pairs per request. Typically both don't need to be changed. Data and metadata in the `SETUP` frame is optional. On the server side, @@ -244,7 +244,7 @@ can be registered as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RSocketStrategies strategies = RSocketStrategies.builder() .encoders(encoders -> encoders.add(new Jackson2CborEncoder())) @@ -258,7 +258,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val strategies = RSocketStrategies.builder() .encoders { it.add(Jackson2CborEncoder()) } @@ -271,7 +271,7 @@ Kotlin:: ---- ====== -`RSocketStrategies` is designed for re-use. In some scenarios, e.g. client and server in +`RSocketStrategies` is designed for re-use. In some scenarios, for example, client and server in the same application, it may be preferable to declare it in Spring configuration. @@ -288,7 +288,7 @@ infrastructure that's used on a server, but registered programmatically as follo ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RSocketStrategies strategies = RSocketStrategies.builder() .routeMatcher(new PathPatternRouteMatcher()) // <1> @@ -308,7 +308,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val strategies = RSocketStrategies.builder() .routeMatcher(PathPatternRouteMatcher()) // <1> @@ -335,7 +335,7 @@ you can still declare `RSocketMessageHandler` as a Spring bean and then apply as ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext context = ... ; RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class); @@ -347,7 +347,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -361,7 +361,7 @@ Kotlin:: ====== For the above you may also need to use `setHandlerPredicate` in `RSocketMessageHandler` to -switch to a different strategy for detecting client responders, e.g. based on a custom +switch to a different strategy for detecting client responders, for example, based on a custom annotation such as `@RSocketClientResponder` vs the default `@Controller`. This is necessary in scenarios with client and server, or multiple clients in the same application. @@ -381,7 +381,7 @@ at that level as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RSocketRequester requester = RSocketRequester.builder() .rsocketConnector(connector -> { @@ -392,7 +392,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val requester = RSocketRequester.builder() .rsocketConnector { @@ -419,7 +419,7 @@ decoupled from handling. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ConnectMapping Mono handle(RSocketRequester requester) { @@ -436,7 +436,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ConnectMapping suspend fun handle(requester: RSocketRequester) { @@ -464,7 +464,7 @@ xref:rsocket.adoc#rsocket-requester-server[server] requester, you can make reque ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ViewBox viewBox = ... ; @@ -479,7 +479,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val viewBox: ViewBox = ... @@ -516,7 +516,7 @@ The `data(Object)` step is optional. Skip it for requests that don't send data: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono location = requester.route("find.radar.EWR")) .retrieveMono(AirportLocation.class); @@ -524,7 +524,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.messaging.rsocket.retrieveAndAwait @@ -534,14 +534,14 @@ Kotlin:: ====== Extra metadata values can be added if using -{gh-rsocket-extensions}/CompositeMetadata.md[composite metadata] (the default) and if the +{rsocket-protocol-extensions}/CompositeMetadata.md[composite metadata] (the default) and if the values are supported by a registered `Encoder`. For example: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String securityToken = ... ; ViewBox viewBox = ... ; @@ -555,7 +555,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.messaging.rsocket.retrieveFlow @@ -600,7 +600,7 @@ methods: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration static class ServerConfig { @@ -616,7 +616,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class ServerConfig { @@ -636,7 +636,7 @@ Then start an RSocket server through the Java RSocket API and plug the ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext context = ... ; RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class); @@ -649,7 +649,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.beans.factory.getBean @@ -663,8 +663,8 @@ Kotlin:: ====== `RSocketMessageHandler` supports -{gh-rsocket-extensions}/CompositeMetadata.md[composite] and -{gh-rsocket-extensions}/Routing.md[routing] metadata by default. You can set its +{rsocket-protocol-extensions}/CompositeMetadata.md[composite] and +{rsocket-protocol-extensions}/Routing.md[routing] metadata by default. You can set its xref:rsocket.adoc#rsocket-metadata-extractor[MetadataExtractor] if you need to switch to a different mime type or register additional metadata mime types. @@ -684,7 +684,7 @@ you need to share configuration between a client and a server in the same proces ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration static class ServerConfig { @@ -709,7 +709,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class ServerConfig { @@ -751,7 +751,7 @@ xref:rsocket.adoc#rsocket-annot-responders-client[client] responder configuratio ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class RadarsController { @@ -765,7 +765,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class RadarsController { @@ -798,7 +798,7 @@ use the following method arguments: | Requester for making requests to the remote end. | `@DestinationVariable` -| Value extracted from the route based on variables in the mapping pattern, e.g. +| Value extracted from the route based on variables in the mapping pattern, for example, pass:q[`@MessageMapping("find.radar.{id}")`]. | `@Header` @@ -879,7 +879,7 @@ For example, to handle requests as a responder: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public interface RadarsService { @@ -898,7 +898,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- interface RadarsService { @@ -954,8 +954,8 @@ xref:rsocket.adoc#rsocket-requester-server[Server Requester] for details. == MetadataExtractor Responders must interpret metadata. -{gh-rsocket-extensions}/CompositeMetadata.md[Composite metadata] allows independently -formatted metadata values (e.g. for routing, security, tracing) each with its own mime +{rsocket-protocol-extensions}/CompositeMetadata.md[Composite metadata] allows independently +formatted metadata values (for example, for routing, security, tracing) each with its own mime type. Applications need a way to configure metadata mime types to support, and a way to access extracted values. @@ -965,7 +965,7 @@ in annotated handler methods. `DefaultMetadataExtractor` can be given `Decoder` instances to decode metadata. Out of the box it has built-in support for -{gh-rsocket-extensions}/Routing.md["message/x.rsocket.routing.v0"] which it decodes to +{rsocket-protocol-extensions}/Routing.md["message/x.rsocket.routing.v0"] which it decodes to `String` and saves under the "route" key. For any other mime type you'll need to provide a `Decoder` and register the mime type as follows: @@ -973,7 +973,7 @@ a `Decoder` and register the mime type as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders); extractor.metadataToExtract(fooMimeType, Foo.class, "foo"); @@ -981,7 +981,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.messaging.rsocket.metadataToExtract @@ -999,7 +999,7 @@ map. Here is an example where JSON is used for metadata: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders); extractor.metadataToExtract( @@ -1012,7 +1012,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.messaging.rsocket.metadataToExtract @@ -1031,7 +1031,7 @@ simply use a callback to customize registrations as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RSocketStrategies strategies = RSocketStrategies.builder() .metadataExtractorRegistry(registry -> { @@ -1043,7 +1043,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.messaging.rsocket.metadataToExtract @@ -1115,7 +1115,9 @@ method parameters: | `@Payload` | Set the input payload(s) for the request. This can be a concrete value, or any producer of values that can be adapted to a Reactive Streams `Publisher` via - `ReactiveAdapterRegistry` + `ReactiveAdapterRegistry`. A payload must be provided unless the `required` attribute + is set to `false`, or the parameter is marked optional as determined by + {spring-framework-api}/core/MethodParameter.html#isOptional()[`MethodParameter#isOptional`]. | `Object`, if followed by `MimeType` | The value for a metadata entry in the input payload. This can be any `Object` as long 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 e9cd1e17679f..41b16558e1aa 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 @@ -11,6 +11,7 @@ xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupite * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[`@NestedTestConfiguration`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-enabledif[`@EnabledIf`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-disabledif[`@DisabledIf`] +* xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`] [[integration-testing-annotations-junit-jupiter-springjunitconfig]] == `@SpringJUnitConfig` @@ -29,7 +30,7 @@ configuration class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) // <1> class ConfigurationClassJUnitJupiterSpringTests { @@ -40,7 +41,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) // <1> class ConfigurationClassJUnitJupiterSpringTests { @@ -58,7 +59,7 @@ location of a configuration file: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(locations = "/test-config.xml") // <1> class XmlJUnitJupiterSpringTests { @@ -69,7 +70,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(locations = ["/test-config.xml"]) // <1> class XmlJUnitJupiterSpringTests { @@ -81,7 +82,7 @@ Kotlin:: See xref:testing/testcontext-framework/ctx-management.adoc[Context Management] as well as the javadoc for -{api-spring-framework}/test/context/junit/jupiter/SpringJUnitConfig.html[`@SpringJUnitConfig`] +{spring-framework-api}/test/context/junit/jupiter/SpringJUnitConfig.html[`@SpringJUnitConfig`] and `@ContextConfiguration` for further details. [[integration-testing-annotations-junit-jupiter-springjunitwebconfig]] @@ -104,7 +105,7 @@ a configuration class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig(TestConfig.class) // <1> class ConfigurationClassJUnitJupiterSpringWebTests { @@ -115,7 +116,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig(TestConfig::class) // <1> class ConfigurationClassJUnitJupiterSpringWebTests { @@ -133,7 +134,7 @@ location of a configuration file: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig(locations = "/test-config.xml") // <1> class XmlJUnitJupiterSpringWebTests { @@ -144,7 +145,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig(locations = ["/test-config.xml"]) // <1> class XmlJUnitJupiterSpringWebTests { @@ -156,23 +157,23 @@ Kotlin:: See xref:testing/testcontext-framework/ctx-management.adoc[Context Management] as well as the javadoc for -{api-spring-framework}/test/context/junit/jupiter/web/SpringJUnitWebConfig.html[`@SpringJUnitWebConfig`], -{api-spring-framework}/test/context/ContextConfiguration.html[`@ContextConfiguration`], and -{api-spring-framework}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] +{spring-framework-api}/test/context/junit/jupiter/web/SpringJUnitWebConfig.html[`@SpringJUnitWebConfig`], +{spring-framework-api}/test/context/ContextConfiguration.html[`@ContextConfiguration`], and +{spring-framework-api}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] for further details. [[integration-testing-annotations-testconstructor]] == `@TestConstructor` -`@TestConstructor` is a type-level annotation that is used to configure how the parameters -of a test class constructor are autowired from components in the test's +`@TestConstructor` is an annotation that can be applied to a test class to configure how +the parameters of a test class constructor are autowired from components in the test's `ApplicationContext`. 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] @@ -182,25 +183,24 @@ The default _test constructor autowire mode_ can be changed by setting the default mode may be set via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. -As of Spring Framework 5.3, the default mode may also be configured as a +The default mode may also be configured as a https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter]. If the `spring.test.constructor.autowire.mode` property is not set, test class constructors will not be automatically autowired. ===== -NOTE: As of Spring Framework 5.2, `@TestConstructor` is only supported in conjunction -with the `SpringExtension` for use with JUnit Jupiter. Note that the `SpringExtension` is -often automatically registered for you – for example, when using annotations such as -`@SpringJUnitConfig` and `@SpringJUnitWebConfig` or various test-related annotations from -Spring Boot Test. +NOTE: `@TestConstructor` is only supported in conjunction with the `SpringExtension` for +use with JUnit Jupiter. Note that the `SpringExtension` is often automatically registered +for you – for example, when using annotations such as `@SpringJUnitConfig` and +`@SpringJUnitWebConfig` or various test-related annotations from Spring Boot Test. [[integration-testing-annotations-nestedtestconfiguration]] == `@NestedTestConfiguration` -`@NestedTestConfiguration` is a type-level annotation that is used to configure how -Spring test configuration annotations are processed within enclosing class hierarchies -for inner test classes. +`@NestedTestConfiguration` is an annotation that can be applied to a test class to +configure how Spring test configuration annotations are processed within enclosing class +hierarchies for inner test classes. If `@NestedTestConfiguration` is not present or meta-present on a test class, in its supertype hierarchy, or in its enclosing class hierarchy, the default _enclosing @@ -274,7 +274,7 @@ example, you can create a custom `@EnabledOnMac` annotation as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @@ -287,7 +287,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) @@ -340,7 +340,7 @@ example, you can create a custom `@DisabledOnMac` annotation as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @@ -353,7 +353,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) 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 8c4c645d95af..17fdf5c9bef0 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc @@ -13,10 +13,10 @@ xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runne [[integration-testing-annotations-junit4-ifprofilevalue]] == `@IfProfileValue` -`@IfProfileValue` indicates that the annotated test is enabled for a specific testing -environment. If the configured `ProfileValueSource` returns a matching `value` for the -provided `name`, the test is enabled. Otherwise, the test is disabled and, effectively, -ignored. +`@IfProfileValue` indicates that the annotated test class or test method is enabled for a +specific testing environment. If the configured `ProfileValueSource` returns a matching +`value` for the provided `name`, the test is enabled. Otherwise, the test is disabled +and, effectively, ignored. You can apply `@IfProfileValue` at the class level, the method level, or both. Class-level usage of `@IfProfileValue` takes precedence over method-level usage for any @@ -31,7 +31,7 @@ The following example shows a test that has an `@IfProfileValue` annotation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @IfProfileValue(name="java.vendor", value="Oracle Corporation") // <1> @Test @@ -43,7 +43,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @IfProfileValue(name="java.vendor", value="Oracle Corporation") // <1> @Test @@ -63,7 +63,7 @@ Consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) // <1> @Test @@ -75,7 +75,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @IfProfileValue(name="test-groups", values=["unit-tests", "integration-tests"]) // <1> @Test @@ -90,17 +90,18 @@ Kotlin:: [[integration-testing-annotations-junit4-profilevaluesourceconfiguration]] == `@ProfileValueSourceConfiguration` -`@ProfileValueSourceConfiguration` is a class-level annotation that specifies what type -of `ProfileValueSource` to use when retrieving profile values configured through the -`@IfProfileValue` annotation. If `@ProfileValueSourceConfiguration` is not declared for a -test, `SystemProfileValueSource` is used by default. The following example shows how to -use `@ProfileValueSourceConfiguration`: +`@ProfileValueSourceConfiguration` is an annotation that can be applied to a test class +to specify what type of `ProfileValueSource` to use when retrieving profile values +configured through the `@IfProfileValue` annotation. If +`@ProfileValueSourceConfiguration` is not declared for a test, `SystemProfileValueSource` +is used by default. The following example shows how to use +`@ProfileValueSourceConfiguration`: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ProfileValueSourceConfiguration(CustomProfileValueSource.class) // <1> public class CustomProfileValueSourceTests { @@ -111,7 +112,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ProfileValueSourceConfiguration(CustomProfileValueSource::class) // <1> class CustomProfileValueSourceTests { @@ -137,7 +138,7 @@ example shows how to use it: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Timed(millis = 1000) // <1> public void testProcessWithOneSecondTimeout() { @@ -148,7 +149,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Timed(millis = 1000) // <1> fun testProcessWithOneSecondTimeout() { @@ -182,7 +183,7 @@ following example shows how to use the `@Repeat` annotation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Repeat(10) // <1> @Test @@ -194,7 +195,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repeat(10) // <1> @Test 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 20bb7976d6a0..ce2381c7c1c5 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc @@ -43,7 +43,7 @@ Consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RunWith(SpringRunner.class) @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) @@ -60,7 +60,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RunWith(SpringRunner::class) @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") @@ -84,7 +84,7 @@ that centralizes the common test configuration for Spring, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -96,7 +96,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @@ -114,7 +114,7 @@ configuration of individual JUnit 4 based test classes, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RunWith(SpringRunner.class) @TransactionalDevTestConfig @@ -127,7 +127,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RunWith(SpringRunner::class) @TransactionalDevTestConfig @@ -147,7 +147,7 @@ example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) @@ -164,7 +164,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") @@ -189,7 +189,7 @@ as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -202,7 +202,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @@ -221,7 +221,7 @@ configuration of individual JUnit Jupiter based test classes, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @TransactionalDevTestConfig class OrderRepositoryTests { } @@ -232,7 +232,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @TransactionalDevTestConfig class OrderRepositoryTests { } @@ -253,7 +253,7 @@ follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @@ -265,7 +265,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @@ -283,7 +283,7 @@ configuration of individual JUnit Jupiter based test methods, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @TransactionalIntegrationTest void saveOrder() { } @@ -294,7 +294,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @TransactionalIntegrationTest fun saveOrder() { } @@ -305,5 +305,5 @@ Kotlin:: ====== For further details, see the -https://github.com/spring-projects/spring-framework/wiki/Spring-Annotation-Programming-Model[Spring Annotation Programming Model] +{spring-framework-wiki}/Spring-Annotation-Programming-Model[Spring Annotation Programming Model] wiki page. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc index 45faf7965d4a..300c32dc8911 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc @@ -16,6 +16,8 @@ Spring's testing annotations include the following: * xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[`@ActiveProfiles`] * xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`] * xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[`@DynamicPropertySource`] +* xref:testing/annotations/integration-spring/annotation-testbean.adoc[`@TestBean`] +* xref:testing/annotations/integration-spring/annotation-mockitobean.adoc[`@MockitoBean` and `@MockitoSpyBean`] * xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[`@DirtiesContext`] * xref:testing/annotations/integration-spring/annotation-testexecutionlisteners.adoc[`@TestExecutionListeners`] * xref:testing/annotations/integration-spring/annotation-recordapplicationevents.adoc[`@RecordApplicationEvents`] @@ -27,4 +29,5 @@ Spring's testing annotations include the following: * xref:testing/annotations/integration-spring/annotation-sqlconfig.adoc[`@SqlConfig`] * xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[`@SqlMergeMode`] * xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[`@SqlGroup`] +* xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`] diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-activeprofiles.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-activeprofiles.adoc index 6b3d521b492b..62233f402d9b 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-activeprofiles.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-activeprofiles.adoc @@ -1,8 +1,8 @@ [[spring-testing-annotation-activeprofiles]] = `@ActiveProfiles` -`@ActiveProfiles` is a class-level annotation that is used to declare which bean -definition profiles should be active when loading an `ApplicationContext` for an +`@ActiveProfiles` is an annotation that can be applied to a test class to declare which +bean definition profiles should be active when loading an `ApplicationContext` for an integration test. The following example indicates that the `dev` profile should be active: @@ -11,7 +11,7 @@ The following example indicates that the `dev` profile should be active: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @ActiveProfiles("dev") // <1> @@ -23,7 +23,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @ActiveProfiles("dev") // <1> @@ -42,7 +42,7 @@ be active: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @ActiveProfiles({"dev", "integration"}) // <1> @@ -54,7 +54,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @ActiveProfiles(["dev", "integration"]) // <1> @@ -74,6 +74,6 @@ and registering it by using the `resolver` attribute of `@ActiveProfiles`. See xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[Context Configuration with Environment Profiles], xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration], and the -{api-spring-framework}/test/context/ActiveProfiles.html[`@ActiveProfiles`] javadoc for +{spring-framework-api}/test/context/ActiveProfiles.html[`@ActiveProfiles`] javadoc for examples and further details. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-aftertransaction.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-aftertransaction.adoc index 3a2e3e73eaad..daa7c6995cf6 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-aftertransaction.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-aftertransaction.adoc @@ -11,7 +11,7 @@ methods. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @AfterTransaction // <1> void afterTransaction() { @@ -22,7 +22,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @AfterTransaction // <1> fun afterTransaction() { diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beforetransaction.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beforetransaction.adoc index 6bf76783406c..dc3cc242294c 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beforetransaction.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beforetransaction.adoc @@ -13,7 +13,7 @@ The following example shows how to use the `@BeforeTransaction` annotation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @BeforeTransaction // <1> void beforeTransaction() { @@ -24,7 +24,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @BeforeTransaction // <1> fun beforeTransaction() { diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-bootstrapwith.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-bootstrapwith.adoc index 5769b3d3d3f3..a297b0149944 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-bootstrapwith.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-bootstrapwith.adoc @@ -2,8 +2,8 @@ = `@BootstrapWith` :page-section-summary-toc: 1 -`@BootstrapWith` is a class-level annotation that you can use to configure how the Spring -TestContext Framework is bootstrapped. Specifically, you can use `@BootstrapWith` to -specify a custom `TestContextBootstrapper`. See the section on +`@BootstrapWith` is an annotation that can be applied to a test class to configure how +the Spring TestContext Framework is bootstrapped. Specifically, you can use +`@BootstrapWith` to specify a custom `TestContextBootstrapper`. See the section on xref:testing/testcontext-framework/bootstrapping.adoc[bootstrapping the TestContext framework] for further details. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-commit.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-commit.adoc index 46440b35ced0..555f8de110eb 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-commit.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-commit.adoc @@ -13,7 +13,7 @@ The following example shows how to use the `@Commit` annotation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Commit // <1> @Test @@ -25,7 +25,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Commit // <1> @Test diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextconfiguration.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextconfiguration.adoc index 47294f5adae4..30b7e0e2ced8 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextconfiguration.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextconfiguration.adoc @@ -1,10 +1,10 @@ [[spring-testing-annotation-contextconfiguration]] = `@ContextConfiguration` -`@ContextConfiguration` defines class-level metadata that is used to determine how to -load and configure an `ApplicationContext` for integration tests. Specifically, -`@ContextConfiguration` declares the application context resource `locations` or the -component `classes` used to load the context. +`@ContextConfiguration` is an annotation that can be applied to a test class to configure +metadata that is used to determine how to load and configure an `ApplicationContext` for +integration tests. Specifically, `@ContextConfiguration` declares the application context +resource `locations` or the component `classes` used to load the context. Resource locations are typically XML configuration files or Groovy scripts located in the classpath, while component classes are typically `@Configuration` classes. However, @@ -19,7 +19,7 @@ file: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration("/test-config.xml") // <1> class XmlApplicationContextTests { @@ -30,7 +30,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration("/test-config.xml") // <1> class XmlApplicationContextTests { @@ -47,7 +47,7 @@ The following example shows a `@ContextConfiguration` annotation that refers to ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration(classes = TestConfig.class) // <1> class ConfigClassApplicationContextTests { @@ -58,7 +58,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration(classes = [TestConfig::class]) // <1> class ConfigClassApplicationContextTests { @@ -77,7 +77,7 @@ The following example shows such a case: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration(initializers = CustomContextInitializer.class) // <1> class ContextInitializerTests { @@ -88,7 +88,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration(initializers = [CustomContextInitializer::class]) // <1> class ContextInitializerTests { @@ -110,7 +110,7 @@ The following example uses both a location and a loader: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) // <1> class CustomLoaderXmlApplicationContextTests { @@ -121,7 +121,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration("/test-context.xml", loader = CustomContextLoader::class) // <1> class CustomLoaderXmlApplicationContextTests { diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc index 497ef55afe12..5bd9ecc41ece 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc @@ -1,10 +1,10 @@ [[spring-testing-annotation-contextcustomizerfactories]] = `@ContextCustomizerFactories` -`@ContextCustomizerFactories` is used to register `ContextCustomizerFactory` -implementations for a particular test class, its subclasses, and its nested classes. If -you wish to register a factory globally, you should register it via the automatic -discovery mechanism described in +`@ContextCustomizerFactories` is an annotation that can be applied to a test class to +register `ContextCustomizerFactory` implementations for the particular test class, its +subclasses, and its nested classes. If you wish to register a factory globally, you +should register it via the automatic discovery mechanism described in xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[`ContextCustomizerFactory` Configuration]. The following example shows how to register two `ContextCustomizerFactory` implementations: @@ -13,7 +13,7 @@ The following example shows how to register two `ContextCustomizerFactory` imple ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @ContextCustomizerFactories({CustomContextCustomizerFactory.class, AnotherContextCustomizerFactory.class}) // <1> @@ -25,7 +25,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @ContextCustomizerFactories([CustomContextCustomizerFactory::class, AnotherContextCustomizerFactory::class]) // <1> @@ -40,6 +40,6 @@ Kotlin:: By default, `@ContextCustomizerFactories` provides support for inheriting factories from superclasses or enclosing classes. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] and the -{api-spring-framework}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories` +{spring-framework-api}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories` javadoc] for an example and further details. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contexthierarchy.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contexthierarchy.adoc index f136a4da5c99..a031e048e8e4 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contexthierarchy.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-contexthierarchy.adoc @@ -1,18 +1,18 @@ [[spring-testing-annotation-contexthierarchy]] = `@ContextHierarchy` -`@ContextHierarchy` is a class-level annotation that is used to define a hierarchy of -`ApplicationContext` instances for integration tests. `@ContextHierarchy` should be -declared with a list of one or more `@ContextConfiguration` instances, each of which -defines a level in the context hierarchy. The following examples demonstrate the use of -`@ContextHierarchy` within a single test class (`@ContextHierarchy` can also be used -within a test class hierarchy): +`@ContextHierarchy` is an annotation that can be applied to a test class to define a +hierarchy of `ApplicationContext` instances for integration tests. `@ContextHierarchy` +should be declared with a list of one or more `@ContextConfiguration` instances, each of +which defines a level in the context hierarchy. The following examples demonstrate the +use of `@ContextHierarchy` within a single test class (`@ContextHierarchy` can also be +used within a test class hierarchy): [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextHierarchy({ @ContextConfiguration("/parent-config.xml"), @@ -25,7 +25,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextHierarchy( ContextConfiguration("/parent-config.xml"), @@ -40,7 +40,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @WebAppConfiguration @ContextHierarchy({ @@ -54,7 +54,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @WebAppConfiguration @ContextHierarchy( @@ -70,6 +70,6 @@ If you need to merge or override the configuration for a given level of the cont hierarchy within a test class hierarchy, you must explicitly name that level by supplying the same value to the `name` attribute in `@ContextConfiguration` at each corresponding level in the class hierarchy. See xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[Context Hierarchies] and the -{api-spring-framework}/test/context/ContextHierarchy.html[`@ContextHierarchy`] javadoc +{spring-framework-api}/test/context/ContextHierarchy.html[`@ContextHierarchy`] javadoc for further examples. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dirtiescontext.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dirtiescontext.adoc index 39c27a8f1600..4f7dd2399468 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dirtiescontext.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dirtiescontext.adoc @@ -10,9 +10,9 @@ rebuilt for any subsequent test that requires a context with the same configurat metadata. You can use `@DirtiesContext` as both a class-level and a method-level annotation within -the same class or class hierarchy. In such scenarios, the `ApplicationContext` is marked -as dirty before or after any such annotated method as well as before or after the current -test class, depending on the configured `methodMode` and `classMode`. When +the same test class or test class hierarchy. In such scenarios, the `ApplicationContext` +is marked as dirty before or after any such annotated method as well as before or after +the current test class, depending on the configured `methodMode` and `classMode`. When `@DirtiesContext` is declared at both the class level and the method level, the configured modes from both annotations will be honored. For example, if the class mode is set to `BEFORE_EACH_TEST_METHOD` and the method mode is set to `AFTER_METHOD`, the @@ -28,7 +28,7 @@ configuration scenarios: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext(classMode = BEFORE_CLASS) // <1> class FreshContextTests { @@ -39,7 +39,7 @@ Java:: + Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext(classMode = BEFORE_CLASS) // <1> class FreshContextTests { @@ -56,7 +56,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext // <1> class ContextDirtyingTests { @@ -67,7 +67,7 @@ Java:: + Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext // <1> class ContextDirtyingTests { @@ -85,7 +85,7 @@ mode set to `BEFORE_EACH_TEST_METHOD.` ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) // <1> class FreshContextTests { @@ -96,7 +96,7 @@ Java:: + Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) // <1> class FreshContextTests { @@ -114,7 +114,7 @@ mode set to `AFTER_EACH_TEST_METHOD.` ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) // <1> class ContextDirtyingTests { @@ -125,7 +125,7 @@ Java:: + Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) // <1> class ContextDirtyingTests { @@ -143,7 +143,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext(methodMode = BEFORE_METHOD) // <1> @Test @@ -155,7 +155,7 @@ Java:: + Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext(methodMode = BEFORE_METHOD) // <1> @Test @@ -173,7 +173,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext // <1> @Test @@ -185,7 +185,7 @@ Java:: + Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @DirtiesContext // <1> @Test @@ -211,7 +211,7 @@ as the following example shows. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextHierarchy({ @ContextConfiguration("/parent-config.xml"), @@ -234,7 +234,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextHierarchy( ContextConfiguration("/parent-config.xml"), @@ -257,6 +257,6 @@ Kotlin:: For further details regarding the `EXHAUSTIVE` and `CURRENT_LEVEL` algorithms, see the -{api-spring-framework}/test/annotation/DirtiesContext.HierarchyMode.html[`DirtiesContext.HierarchyMode`] +{spring-framework-api}/test/annotation/DirtiesContext.HierarchyMode.html[`DirtiesContext.HierarchyMode`] javadoc. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-disabledinaotmode.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-disabledinaotmode.adoc new file mode 100644 index 000000000000..be7689d1b269 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-disabledinaotmode.adoc @@ -0,0 +1,20 @@ +[[spring-testing-annotation-disabledinaotmode]] += `@DisabledInAotMode` + +`@DisabledInAotMode` signals that the annotated test class is disabled in Spring AOT +(ahead-of-time) mode, which means that the `ApplicationContext` for the test class will +not be processed for AOT optimizations at build time. + +If a test class is annotated with `@DisabledInAotMode`, all other test classes which +specify configuration to load the same `ApplicationContext` must also be annotated with +`@DisabledInAotMode`. Failure to annotate all such test classes will result in an +exception, either at build time or run time. + +When used with JUnit Jupiter based tests, `@DisabledInAotMode` also signals that the +annotated test class or test method is disabled when running the test suite in Spring AOT +mode. When applied at the class level, all test methods within that class will be +disabled. In this sense, `@DisabledInAotMode` has semantics similar to those of JUnit +Jupiter's `@DisabledInNativeImage` annotation. + +For details on AOT support specific to integration tests, see +xref:testing/testcontext-framework/aot.adoc[Ahead of Time Support for Tests]. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc index 003175517ccf..3aeccb7f8cf4 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc @@ -1,12 +1,12 @@ [[spring-testing-annotation-dynamicpropertysource]] = `@DynamicPropertySource` -`@DynamicPropertySource` is a method-level annotation that you can use to register -_dynamic_ properties to be added to the set of `PropertySources` in the `Environment` for -an `ApplicationContext` loaded for an integration test. Dynamic properties are useful -when you do not know the value of the properties upfront – for example, if the properties -are managed by an external resource such as for a container managed by the -https://www.testcontainers.org/[Testcontainers] project. +`@DynamicPropertySource` is an annotation that can be applied to methods in integration +test classes that need to register _dynamic_ properties to be added to the set of +`PropertySources` in the `Environment` for an `ApplicationContext` loaded for an +integration test. Dynamic properties are useful when you do not know the value of the +properties upfront – for example, if the properties are managed by an external resource +such as for a container managed by the {testcontainers-site}[Testcontainers] project. The following example demonstrates how to register a dynamic property: @@ -14,7 +14,7 @@ The following example demonstrates how to register a dynamic property: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration class MyIntegrationTests { @@ -35,7 +35,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration class MyIntegrationTests { 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 new file mode 100644 index 000000000000..f92d5c584afa --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc @@ -0,0 +1,304 @@ +[[spring-testing-annotation-beanoverriding-mockitobean]] += `@MockitoBean` and `@MockitoSpyBean` + +{spring-framework-api}/test/context/bean/override/mockito/MockitoBean.html[`@MockitoBean`] and +{spring-framework-api}/test/context/bean/override/mockito/MockitoSpyBean.html[`@MockitoSpyBean`] +can be used in test classes to override a bean in the test's `ApplicationContext` with a +Mockito _mock_ or _spy_, respectively. In the latter case, an early instance of the +original bean is captured and wrapped by the spy. + +The annotations can be applied in the following ways. + +* On a non-static field in a test class or any of its superclasses. +* On a non-static field in an enclosing class for a `@Nested` test class or in any class + in the type hierarchy or enclosing class hierarchy above the `@Nested` test class. +* At the type level on a test class or any superclass or implemented interface in the + type hierarchy above the test class. +* At the type level on an enclosing class for a `@Nested` test class or on any class or + interface in the type hierarchy or enclosing class hierarchy above the `@Nested` test + class. + +When `@MockitoBean` or `@MockitoSpyBean` is declared on a field, the bean to mock or spy +is inferred from the type of the annotated field. If multiple candidates exist in the +`ApplicationContext`, a `@Qualifier` annotation can be declared on the field to help +disambiguate. In the absence of a `@Qualifier` annotation, the name of the annotated +field will be used as a _fallback qualifier_. Alternatively, you can explicitly specify a +bean name to mock or spy by setting the `value` or `name` attribute in the annotation. + +When `@MockitoBean` or `@MockitoSpyBean` is declared at the type level, the type of bean +(or beans) to mock or spy must be supplied via the `types` attribute in the annotation – +for example, `@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple +candidates exist in the `ApplicationContext`, you can explicitly specify a bean name to +mock or spy by setting the `name` attribute. Note, however, that the `types` attribute +must contain a single type if an explicit bean `name` is configured – for example, +`@MockitoBean(name = "ps1", types = PrintingService.class)`. + +To support reuse of mock configuration, `@MockitoBean` and `@MockitoSpyBean` may be used +as meta-annotations to create custom _composed annotations_ – for example, to define +common mock or spy configuration in a single annotation that can be reused across a test +suite. `@MockitoBean` and `@MockitoSpyBean` can also be used as repeatable annotations at +the type level — for example, to mock or spy several beans by name. + +[WARNING] +==== +Qualifiers, including the name of a field, are used to determine if a separate +`ApplicationContext` needs to be created. If you are using this feature to mock or spy +the same bean in several test classes, make sure to name the fields consistently to avoid +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` +xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy for bean overrides]. +If a corresponding bean does not exist, a new bean will be created. However, you can +switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to `true` – +for example, `@MockitoBean(enforceOverride = true)`. + +The `@MockitoSpyBean` annotation uses the `WRAP` +xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy], +and the original instance is wrapped in a Mockito spy. This strategy requires that +exactly one candidate bean exists. + +[TIP] +==== +Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean +will result in an exception. + +When using `@MockitoBean` to mock a bean created by a `FactoryBean`, the `FactoryBean` +will be replaced with a singleton mock of the type of object created by the `FactoryBean`. + +When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be created +for the object created by the `FactoryBean`, not for the `FactoryBean` itself. +==== + +[NOTE] +==== +There are no restrictions on the visibility of `@MockitoBean` and `@MockitoSpyBean` +fields. + +Such fields can therefore be `public`, `protected`, package-private (default visibility), +or `private` depending on the needs or coding practices of the project. +==== + +[[spring-testing-annotation-beanoverriding-mockitobean-examples]] +== `@MockitoBean` Examples + +The following example shows how to use the default behavior of the `@MockitoBean` +annotation. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + @MockitoBean // <1> + CustomService customService; + + // tests... + } +---- +<1> Replace the bean with type `CustomService` with a Mockito mock. +====== + +In the example above, we are creating a mock for `CustomService`. If more than one bean +of that type exists, the bean named `customService` is considered. Otherwise, the test +will fail, and you will need to provide a qualifier of some sort to identify which of the +`CustomService` beans you want to override. If no such bean exists, a bean will be +created with an auto-generated bean name. + +The following example uses a by-name lookup, rather than a by-type lookup. If no bean +named `service` exists, one is created. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + @MockitoBean("service") // <1> + CustomService customService; + + // tests... + + } +---- +<1> Replace the bean named `service` with a Mockito mock. +====== + +The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @MockitoBean(types = {OrderService.class, UserService.class}) // <1> + @MockitoBean(name = "ps1", types = PrintingService.class) // <2> + public @interface SharedMocks { + } +---- +<1> Register `OrderService` and `UserService` mocks by-type. +<2> Register `PrintingService` mock by-name. +====== + +The following demonstrates how `@SharedMocks` can be used on a test class. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + @SharedMocks // <1> + class BeanOverrideTests { + + @Autowired OrderService orderService; // <2> + + @Autowired UserService userService; // <2> + + @Autowired PrintingService ps1; // <2> + + // Inject other components that rely on the mocks. + + @Test + void testThatDependsOnMocks() { + // ... + } + } +---- +<1> Register common mocks via the custom `@SharedMocks` annotation. +<2> Optionally inject mocks to _stub_ or _verify_ them. +====== + +TIP: The mocks can also be injected into `@Configuration` classes or other test-related +components in the `ApplicationContext` in order to configure them with Mockito's stubbing +APIs. + +[[spring-testing-annotation-beanoverriding-mockitospybean-examples]] +== `@MockitoSpyBean` Examples + +The following example shows how to use the default behavior of the `@MockitoSpyBean` +annotation. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + @MockitoSpyBean // <1> + CustomService customService; + + // tests... + } +---- +<1> Wrap the bean with type `CustomService` with a Mockito spy. +====== + +In the example above, we are wrapping the bean with type `CustomService`. If more than +one bean of that type exists, the bean named `customService` is considered. Otherwise, +the test will fail, and you will need to provide a qualifier of some sort to identify +which of the `CustomService` beans you want to spy. + +The following example uses a by-name lookup, rather than a by-type lookup. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + @MockitoSpyBean("service") // <1> + CustomService customService; + + // tests... + } +---- +<1> Wrap the bean named `service` with a Mockito spy. +====== + +The following `@SharedSpies` annotation registers two spies by-type and one spy by-name. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @MockitoSpyBean(types = {OrderService.class, UserService.class}) // <1> + @MockitoSpyBean(name = "ps1", types = PrintingService.class) // <2> + public @interface SharedSpies { + } +---- +<1> Register `OrderService` and `UserService` spies by-type. +<2> Register `PrintingService` spy by-name. +====== + +The following demonstrates how `@SharedSpies` can be used on a test class. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + @SharedSpies // <1> + class BeanOverrideTests { + + @Autowired OrderService orderService; // <2> + + @Autowired UserService userService; // <2> + + @Autowired PrintingService ps1; // <2> + + // Inject other components that rely on the spies. + + @Test + void testThatDependsOnMocks() { + // ... + } + } +---- +<1> Register common spies via the custom `@SharedSpies` annotation. +<2> Optionally inject spies to _stub_ or _verify_ them. +====== + +TIP: The spies can also be injected into `@Configuration` classes or other test-related +components in the `ApplicationContext` in order to configure them with Mockito's stubbing +APIs. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-recordapplicationevents.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-recordapplicationevents.adoc index fbd7f0cf03f2..15f41b4192bb 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-recordapplicationevents.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-recordapplicationevents.adoc @@ -2,13 +2,13 @@ = `@RecordApplicationEvents` :page-section-summary-toc: 1 -`@RecordApplicationEvents` is a class-level annotation that is used to instruct the -_Spring TestContext Framework_ to record all application events that are published in the -`ApplicationContext` during the execution of a single test. +`@RecordApplicationEvents` is an annotation that can be applied to a test class to +instruct the _Spring TestContext Framework_ to record all application events that are +published in the `ApplicationContext` during the execution of a single test. The recorded events can be accessed via the `ApplicationEvents` API within tests. See xref:testing/testcontext-framework/application-events.adoc[Application Events] and the -{api-spring-framework}/test/context/event/RecordApplicationEvents.html[`@RecordApplicationEvents` +{spring-framework-api}/test/context/event/RecordApplicationEvents.html[`@RecordApplicationEvents` javadoc] for an example and further details. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-rollback.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-rollback.adoc index 93f2e32c6b67..9edd23885459 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-rollback.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-rollback.adoc @@ -19,7 +19,7 @@ result is committed to the database): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Rollback(false) // <1> @Test @@ -31,7 +31,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Rollback(false) // <1> @Test diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sql.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sql.adoc index f84aa6b95017..09289d3f56db 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sql.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sql.adoc @@ -9,7 +9,7 @@ it: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Test @Sql({"/test-schema.sql", "/test-user-data.sql"}) // <1> @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test @Sql("/test-schema.sql", "/test-user-data.sql") // <1> diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlconfig.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlconfig.adoc index 9910dd7b4555..a06c77fbf781 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlconfig.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlconfig.adoc @@ -8,7 +8,7 @@ configured with the `@Sql` annotation. The following example shows how to use it ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Test @Sql( @@ -23,7 +23,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test @Sql("/test-user-data.sql", config = SqlConfig(commentPrefix = "`", separator = "@@")) // <1> diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlgroup.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlgroup.adoc index 104e03a1a3e2..fde8964b3290 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlgroup.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlgroup.adoc @@ -11,7 +11,7 @@ annotation. The following example shows how to declare an SQL group: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Test @SqlGroup({ // <1> @@ -26,7 +26,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test @SqlGroup( // <1> diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlmergemode.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlmergemode.adoc index afb3b91dc220..7cb26ee729dd 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlmergemode.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-sqlmergemode.adoc @@ -15,7 +15,7 @@ The following example shows how to use `@SqlMergeMode` at the class level. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) @Sql("/test-schema.sql") @@ -33,7 +33,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) @Sql("/test-schema.sql") @@ -56,7 +56,7 @@ The following example shows how to use `@SqlMergeMode` at the method level. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) @Sql("/test-schema.sql") @@ -74,7 +74,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) @Sql("/test-schema.sql") 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 new file mode 100644 index 000000000000..4ec33c0c154f --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc @@ -0,0 +1,127 @@ +[[spring-testing-annotation-beanoverriding-testbean]] += `@TestBean` + +{spring-framework-api}/test/context/bean/override/convention/TestBean.html[`@TestBean`] +is used on a non-static field in a test class to override a specific bean in the test's +`ApplicationContext` with an instance provided by a factory method. + +The associated factory method name is derived from the annotated field's name, or the +bean name if specified. The factory method must be `static`, accept no arguments, and +have a return type compatible with the type of the bean to override. To make things more +explicit, or if you'd rather use a different name, the annotation allows for a specific +method name to be provided. + +By default, the annotated field's type is used to search for candidate beans to override. +If multiple candidates match, `@Qualifier` can be provided to narrow the candidate to +override. Alternatively, a candidate whose bean name matches the name of the field will +match. + +A bean will be created if a corresponding bean does not exist. However, if you would like +for the test to fail when a corresponding bean does not exist, you can set the +`enforceOverride` attribute to `true` – for example, `@TestBean(enforceOverride = true)`. + +To use a by-name override rather than a by-type override, specify the `name` attribute +of the annotation. + +[WARNING] +==== +Qualifiers, including the name of the field, are used to determine if a separate +`ApplicationContext` needs to be created. If you are using this feature to override the +same bean in several tests, make sure to name the field consistently to avoid creating +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. + +Such fields and methods can therefore be `public`, `protected`, package-private (default +visibility), or `private` depending on the needs or coding practices of the project. +==== + +The following example shows how to use the default behavior of the `@TestBean` annotation: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + class OverrideBeanTests { + @TestBean // <1> + CustomService customService; + + // test case body... + + static CustomService customService() { // <2> + return new MyFakeCustomService(); + } + } +---- +<1> Mark a field for overriding the bean with type `CustomService`. +<2> The result of this static method will be used as the instance and injected into the field. +====== + +In the example above, we are overriding the bean with type `CustomService`. If more than +one bean of that type exists, the bean named `customService` is considered. Otherwise, +the test will fail, and you will need to provide a qualifier of some sort to identify +which of the `CustomService` beans you want to override. + +The following example uses a by-name lookup, rather than a by-type lookup: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + class OverrideBeanTests { + @TestBean(name = "service", methodName = "createCustomService") // <1> + CustomService customService; + + // test case body... + + static CustomService createCustomService() { // <2> + return new MyFakeCustomService(); + } + } +---- +<1> Mark a field for overriding the bean with name `service`, and specify that the + factory method is named `createCustomService`. +<2> The result of this static method will be used as the instance and injected into the field. +====== + +[TIP] +==== +To locate the factory method to invoke, Spring searches in the class in which the +`@TestBean` field is declared, in one of its superclasses, or in any implemented +interfaces. If the `@TestBean` field is declared in a `@Nested` test class, the enclosing +class hierarchy will also be searched. + +Alternatively, a factory method in an external class can be referenced via its +fully-qualified method name following the syntax `#` +– for example, `methodName = "org.example.TestUtils#createCustomService"`. +==== + +[TIP] +==== +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 `FactoryBean`, the `FactoryBean` will be replaced +with a singleton bean corresponding to the value returned from the `@TestBean` factory +method. +==== diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testexecutionlisteners.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testexecutionlisteners.adoc index 3e9505d15235..aada318fdfa8 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testexecutionlisteners.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testexecutionlisteners.adoc @@ -1,7 +1,7 @@ [[spring-testing-annotation-testexecutionlisteners]] = `@TestExecutionListeners` -`@TestExecutionListeners` is used to register listeners for a particular test class, its +`@TestExecutionListeners` is used to register listeners for the annotated test class, its subclasses, and its nested classes. If you wish to register a listener globally, you should register it via the automatic discovery mechanism described in xref:testing/testcontext-framework/tel-config.adoc[`TestExecutionListener` Configuration]. @@ -12,7 +12,7 @@ The following example shows how to register two `TestExecutionListener` implemen ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) // <1> @@ -24,7 +24,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestExecutionListeners(CustomTestExecutionListener::class, AnotherTestExecutionListener::class) // <1> @@ -39,7 +39,7 @@ Kotlin:: By default, `@TestExecutionListeners` provides support for inheriting listeners from superclasses or enclosing classes. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] and the -{api-spring-framework}/test/context/TestExecutionListeners.html[`@TestExecutionListeners` +{spring-framework-api}/test/context/TestExecutionListeners.html[`@TestExecutionListeners` javadoc] for an example and further details. If you discover that you need to switch back to using the default `TestExecutionListener` implementations, see the note in xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-registering-tels[Registering `TestExecutionListener` Implementations]. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testpropertysource.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testpropertysource.adoc index 157fc1289024..6971ec2209fb 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testpropertysource.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testpropertysource.adoc @@ -1,8 +1,8 @@ [[spring-testing-annotation-testpropertysource]] = `@TestPropertySource` -`@TestPropertySource` is a class-level annotation that you can use to configure the -locations of properties files and inlined properties to be added to the set of +`@TestPropertySource` is an annotation that can be applied to a test class to configure +the locations of properties files and inlined properties to be added to the set of `PropertySources` in the `Environment` for an `ApplicationContext` loaded for an integration test. @@ -12,7 +12,7 @@ The following example demonstrates how to declare a properties file from the cla ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource("/test.properties") // <1> @@ -24,7 +24,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource("/test.properties") // <1> @@ -42,7 +42,7 @@ The following example demonstrates how to declare inlined properties: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) // <1> @@ -54,7 +54,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) // <1> diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-webappconfiguration.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-webappconfiguration.adoc index a367a0ee9451..9256db492a05 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-webappconfiguration.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-webappconfiguration.adoc @@ -1,10 +1,10 @@ [[spring-testing-annotation-webappconfiguration]] = `@WebAppConfiguration` -`@WebAppConfiguration` is a class-level annotation that you can use to declare that the -`ApplicationContext` loaded for an integration test should be a `WebApplicationContext`. -The mere presence of `@WebAppConfiguration` on a test class ensures that a -`WebApplicationContext` is loaded for the test, using the default value of +`@WebAppConfiguration` is an annotation that can be applied to a test class to declare +that the `ApplicationContext` loaded for an integration test should be a +`WebApplicationContext`. The mere presence of `@WebAppConfiguration` on a test class +ensures that a `WebApplicationContext` is loaded for the test, using the default value of `"file:src/main/webapp"` for the path to the root of the web application (that is, the resource base path). The resource base path is used behind the scenes to create a `MockServletContext`, which serves as the `ServletContext` for the test's @@ -17,7 +17,7 @@ The following example shows how to use the `@WebAppConfiguration` annotation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @WebAppConfiguration // <1> @@ -29,7 +29,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @WebAppConfiguration // <1> @@ -52,7 +52,7 @@ resource. The following example shows how to specify a classpath resource: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @WebAppConfiguration("classpath:test-web-resources") // <1> @@ -64,7 +64,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @WebAppConfiguration("classpath:test-web-resources") // <1> @@ -80,6 +80,6 @@ Kotlin:: Note that `@WebAppConfiguration` must be used in conjunction with `@ContextConfiguration`, either within a single test class or within a test class hierarchy. See the -{api-spring-framework}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] +{spring-framework-api}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] javadoc for further details. diff --git a/framework-docs/modules/ROOT/pages/testing/integration.adoc b/framework-docs/modules/ROOT/pages/testing/integration.adoc index a7a4b39729ea..79c2a8321153 100644 --- a/framework-docs/modules/ROOT/pages/testing/integration.adoc +++ b/framework-docs/modules/ROOT/pages/testing/integration.adoc @@ -30,7 +30,7 @@ integration support, and the rest of this chapter then focuses on dedicated topi * xref:testing/support-jdbc.adoc[JDBC Testing Support] * xref:testing/testcontext-framework.adoc[Spring TestContext Framework] * xref:testing/webtestclient.adoc[WebTestClient] -* xref:testing/spring-mvc-test-framework.adoc[MockMvc] +* xref:testing/mockmvc.adoc[MockMvc] * xref:testing/spring-mvc-test-client.adoc[Testing Client Applications] * xref:testing/annotations.adoc[Annotations] diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc.adoc new file mode 100644 index 000000000000..ac42f4be5e53 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc.adoc @@ -0,0 +1,16 @@ +[[mockmvc]] += MockMvc +:page-section-summary-toc: 1 + +MockMvc provides support for testing Spring MVC applications. It performs full Spring MVC +request handling but via mock request and response objects instead of a running server. + +MockMvc can be used on its own to perform requests and verify responses using Hamcrest or +through `MockMvcTester` which provides a fluent API using AssertJ. It can also be used +through the xref:testing/webtestclient.adoc[WebTestClient] where MockMvc is plugged in as +the server to handle requests. The advantage of using `WebTestClient` is that it provides +you the option of working with higher level objects instead of raw data as well as the +ability to switch to full, end-to-end HTTP tests against a live server and use the same +test API. + + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj.adoc new file mode 100644 index 000000000000..aa105c91b2a3 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj.adoc @@ -0,0 +1,16 @@ +[[mockmvc-tester]] += AssertJ Integration +:page-section-summary-toc: 1 + +The AssertJ integration builds on top of plain `MockMvc` with several differences: + +* There is no need to use static imports as both the requests and assertions can be +crafted using a fluent API. +* Unresolved exceptions are handled consistently so that your tests do not need to +throw (or catch) `Exception`. +* By default, the result to assert is complete whether the processing is asynchronous +or not. In other words, there is no need for special handling for Async requests. + +`MockMvcTester` is the entry point for the AssertJ support. It allows to craft the +request and return a result that is AssertJ compatible so that it can be wrapped in +a standard `assertThat()` method. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/assertions.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/assertions.adoc new file mode 100644 index 000000000000..bb3a9ca2bc41 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/assertions.adoc @@ -0,0 +1,49 @@ +[[mockmvc-tester-assertions]] += Defining Expectations + +Assertions work the same way as any AssertJ assertions. The support provides dedicated +assert objects for the various pieces of the `MvcTestResult`, as shown in the following +example: + +include-code::./HotelControllerTests[tag=get,indent=0] + +If a request fails, the exchange does not throw the exception. Rather, you can assert +that the result of the exchange has failed: + +include-code::./HotelControllerTests[tag=failure,indent=0] + +The request could also fail unexpectedly, that is the exception thrown by the handler +has not been handled and is thrown as is. You can still use `.hasFailed()` and +`.failure()` but any attempt to access part of the result will throw an exception as +the exchange hasn't completed. + +[[mockmvc-tester-assertions-json]] +== JSON Support + +The AssertJ support for `MvcTestResult` provides JSON support via `bodyJson()`. + +If https://github.com/jayway/JsonPath[JSONPath] is available, you can apply an expression +on the JSON document. The returned value provides convenient methods to return a dedicated +assert object for the various supported JSON data types: + +include-code::./FamilyControllerTests[tag=extract-asmap,indent=0] + +You can also convert the raw content to any of your data types as long as the message +converter is configured properly: + +include-code::./FamilyControllerTests[tag=extract-convert,indent=0] + +Converting to a target `Class` provides a generic assert object. For more complex types, +you may want to use `AssertFactory` instead that returns a dedicated assert type, if +possible: + +include-code::./FamilyControllerTests[tag=extract-convert-assert-factory,indent=0] + +https://jsonassert.skyscreamer.org[JSONAssert] is also supported. The body of the +response can be matched against a `Resource` or a content. If the content ends with +`.json ` we look for a file matching that name on the classpath: + +include-code::./FamilyControllerTests[tag=assert-file,indent=0] + +If you prefer to use another library, you can provide an implementation of +{spring-framework-api}/test/json/JsonComparator.html[`JsonComparator`]. diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/integration.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/integration.adoc new file mode 100644 index 000000000000..25a77205d9a1 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/integration.adoc @@ -0,0 +1,23 @@ +[[mockmvc-tester-integration]] += MockMvc integration + +If you want to use the AssertJ support but have invested in the original `MockMvc` +API, `MockMvcTester` offers several ways to integrate with it. + +If you have your own `RequestBuilder` implementation, you can trigger the processing +of the request using `perform`. The example below showcases how the query can be +crafted with the original API: + +include-code::./HotelControllerTests[tag=perform,indent=0] + +Similarly, if you have crafted custom matchers that you use with the `.andExpect` feature +of `MockMvc` you can use them via `.matches`. In the example below, we rewrite the +preceding example to assert the status with the `ResultMatcher` implementation that +`MockMvc` provides: + +include-code::./HotelControllerTests[tag=matches,indent=0] + +`MockMvc` also defines a `ResultHandler` contract that lets you execute arbitrary actions +on `MvcResult`. If you have implemented this contract you can invoke it using `.apply`. + + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/requests.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/requests.adoc new file mode 100644 index 000000000000..9889532fc1d2 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/requests.adoc @@ -0,0 +1,83 @@ +[[mockmvc-tester-requests]] += Performing Requests + +This section shows how to use `MockMvcTester` to perform requests and its integration +with AssertJ to verify responses. + +`MockMvcTester` provides a fluent API to compose the request that reuses the same +`MockHttpServletRequestBuilder` as the Hamcrest support, except that there is no need +to import a static method. The builder that is returned is AssertJ-aware so that +wrapping it in the regular `assertThat()` factory method triggers the exchange and +provides access to a dedicated Assert object for `MvcTestResult`. + +Here is a simple example that performs a `POST` on `/hotels/42` and configures the +request to specify an `Accept` header: + +include-code::./HotelControllerTests[tag=post,indent=0] + +AssertJ often consists of multiple `assertThat()` statements to validate the different +parts of the exchange. Rather than having a single statement as in the case above, you +can use `.exchange()` to return a `MvcTestResult` that can be used in multiple +`assertThat` statements: + +include-code::./HotelControllerTests[tag=post-exchange,indent=0] + +You can specify query parameters in URI template style, as the following example shows: + +include-code::./HotelControllerTests[tag=query-parameters,indent=0] + +You can also add Servlet request parameters that represent either query or form +parameters, as the following example shows: + +include-code::./HotelControllerTests[tag=parameters,indent=0] + +If application code relies on Servlet request parameters and does not check the query +string explicitly (as is most often the case), it does not matter which option you use. +Keep in mind, however, that query parameters provided with the URI template are decoded +while request parameters provided through the `param(...)` method are expected to already +be decoded. + + +[[mockmvc-tester-requests-async]] +== Async + +If the processing of the request is done asynchronously, `exchange()` waits for +the completion of the request so that the result to assert is effectively immutable. +The default timeout is 10 seconds but it can be controlled on a request-by-request +basis as shown in the following example: + +include-code::./AsyncControllerTests[tag=duration,indent=0] + +If you prefer to get the raw result and manage the lifecycle of the asynchronous +request yourself, use `asyncExchange` rather than `exchange`. + +[[mockmvc-tester-requests-multipart]] +== Multipart + +You can perform file upload requests that internally use +`MockMultipartHttpServletRequest` so that there is no actual parsing of a multipart +request. Rather, you have to set it up to be similar to the following example: + +include-code::./MultipartControllerTests[tag=snippet,indent=0] + +[[mockmvc-tester-requests-paths]] +== Using Servlet and Context Paths + +In most cases, it is preferable to leave the context path and the Servlet path out of the +request URI. If you must test with the full request URI, be sure to set the `contextPath` +and `servletPath` accordingly so that request mappings work, as the following example +shows: + +include-code::./HotelControllerTests[tag=context-servlet-paths,indent=0] + +In the preceding example, it would be cumbersome to set the `contextPath` and +`servletPath` with every performed request. Instead, you can set up default request +properties, as the following example shows: + +include-code::./HotelControllerTests[tag=default-customizations,indent=0] + +The preceding properties affect every request performed through the `mockMvc` instance. +If the same property is also specified on a given request, it overrides the default +value. That is why the HTTP method and URI in the default request do not matter, since +they must be specified on every request. + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/setup.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/setup.adoc new file mode 100644 index 000000000000..5f0317c0411c --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/setup.adoc @@ -0,0 +1,30 @@ +[[mockmvc-tester-setup]] += Configuring MockMvcTester + +`MockMvcTester` can be setup in one of two ways. One is to point directly to the +controllers you want to test and programmatically configure Spring MVC infrastructure. +The second is to point to Spring configuration with Spring MVC and controller +infrastructure in it. + +TIP: For a comparison of those two modes, check xref:testing/mockmvc/setup-options.adoc[Setup Options]. + +To set up `MockMvcTester` for testing a specific controller, use the following: + +include-code::./AccountControllerStandaloneTests[tag=snippet,indent=0] + +To set up `MockMvcTester` through Spring configuration, use the following: + +include-code::./AccountControllerIntegrationTests[tag=snippet,indent=0] + +`MockMvcTester` can convert the JSON response body, or the result of a JSONPath expression, +to one of your domain object as long as the relevant `HttpMessageConverter` is registered. + +If you use Jackson to serialize content to JSON, the following example registers the +converter: + +include-code::./converter/AccountControllerIntegrationTests[tag=snippet,indent=0] + +NOTE: The above assumes the converter has been registered as a Bean. + +Finally, if you have a `MockMvc` instance handy, you can create a `MockMvcTester` by +providing the `MockMvc` instance to use using the `create` factory method. diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest.adoc new file mode 100644 index 000000000000..46a1ecfa4e18 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest.adoc @@ -0,0 +1,7 @@ +[[mockmvc-server]] += Hamcrest Integration +:page-section-summary-toc: 1 + +Plain `MockMvc` provides an API to build the request using a builder-style approach +that can be initiated with static imports. Hamcrest is used to define expectations and +it provides many out-of-the-box options for common needs. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/async-requests.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc similarity index 93% rename from framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/async-requests.adoc rename to framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc index 9dacf436fd5f..949b9ab8a9bd 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/async-requests.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc @@ -1,4 +1,4 @@ -[[spring-mvc-test-async-requests]] +[[mockmvc-async-requests]] = Async Requests This section shows how to use MockMvc on its own to test asynchronous request handling. @@ -20,7 +20,7 @@ or reactive type such as Reactor `Mono`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* @@ -45,7 +45,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test fun test() { diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/expectations.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/expectations.adoc new file mode 100644 index 000000000000..a7cb4d3b3688 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/expectations.adoc @@ -0,0 +1,251 @@ +[[mockmvc-server-defining-expectations]] += Defining Expectations + +You can define expectations by appending one or more `andExpect(..)` calls after +performing a request, as the following example shows. As soon as one expectation fails, +no other expectations will be asserted. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* + + mockMvc.perform(get("/accounts/1")).andExpect(status().isOk()); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.test.web.servlet.get + + mockMvc.get("/accounts/1").andExpect { + status { isOk() } + } +---- +====== + +You can define multiple expectations by appending `andExpectAll(..)` after performing a +request, as the following example shows. In contrast to `andExpect(..)`, +`andExpectAll(..)` guarantees that all supplied expectations will be asserted and that +all failures will be tracked and reported. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* + + mockMvc.perform(get("/accounts/1")).andExpectAll( + status().isOk(), + content().contentType("application/json;charset=UTF-8")); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.test.web.servlet.get + + mockMvc.get("/accounts/1").andExpectAll { + status { isOk() } + content { contentType(APPLICATION_JSON) } + } +---- +====== + +`MockMvcResultMatchers.*` provides a number of expectations, some of which are further +nested with more detailed expectations. + +Expectations fall in two general categories. The first category of assertions verifies +properties of the response (for example, the response status, headers, and content). +These are the most important results to assert. + +The second category of assertions goes beyond the response. These assertions let you +inspect Spring MVC specific aspects, such as which controller method processed the +request, whether an exception was raised and handled, what the content of the model is, +what view was selected, what flash attributes were added, and so on. They also let you +inspect Servlet specific aspects, such as request and session attributes. + +The following test asserts that binding or validation failed: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + mockMvc.perform(post("/persons")) + .andExpect(status().isOk()) + .andExpect(model().attributeHasErrors("person")); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.test.web.servlet.post + + mockMvc.post("/persons").andExpect { + status { isOk() } + model { + attributeHasErrors("person") + } + } +---- +====== + +Many times, when writing tests, it is useful to dump the results of the performed +request. You can do so as follows, where `print()` is a static import from +`MockMvcResultHandlers`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + mockMvc.perform(post("/persons")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(model().attributeHasErrors("person")); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.test.web.servlet.post + + mockMvc.post("/persons").andDo { + print() + }.andExpect { + status { isOk() } + model { + attributeHasErrors("person") + } + } +---- +====== + +As long as request processing does not cause an unhandled exception, the `print()` method +prints all the available result data to `System.out`. There is also a `log()` method and +two additional variants of the `print()` method, one that accepts an `OutputStream` and +one that accepts a `Writer`. For example, invoking `print(System.err)` prints the result +data to `System.err`, while invoking `print(myWriter)` prints the result data to a custom +writer. If you want to have the result data logged instead of printed, you can invoke the +`log()` method, which logs the result data as a single `DEBUG` message under the +`org.springframework.test.web.servlet.result` logging category. + +In some cases, you may want to get direct access to the result and verify something that +cannot be verified otherwise. This can be achieved by appending `.andReturn()` after all +other expectations, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn(); + // ... +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + var mvcResult = mockMvc.post("/persons").andExpect { status { isOk() } }.andReturn() + // ... +---- +====== + +If all tests repeat the same expectations, you can set up common expectations once when +building the `MockMvc` instance, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + standaloneSetup(new SimpleController()) + .alwaysExpect(status().isOk()) + .alwaysExpect(content().contentType("application/json;charset=UTF-8")) + .build() +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed +---- +====== + +Note that common expectations are always applied and cannot be overridden without +creating a separate `MockMvc` instance. + +When a JSON response content contains hypermedia links created with +{spring-github-org}/spring-hateoas[Spring HATEOAS], you can verify the +resulting links by using JsonPath expressions, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people")); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + mockMvc.get("/people") { + accept(MediaType.APPLICATION_JSON) + }.andExpect { + jsonPath("$.links[?(@.rel == 'self')].href") { + value("http://localhost:8080/people") + } + } +---- +====== + +When XML response content contains hypermedia links created with +{spring-github-org}/spring-hateoas[Spring HATEOAS], you can verify the +resulting links by using XPath expressions: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + Map ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom"); + mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML)) + .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people")); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val ns = mapOf("ns" to "http://www.w3.org/2005/Atom") + mockMvc.get("/handle") { + accept(MediaType.APPLICATION_XML) + }.andExpect { + xpath("/person/ns:link[@rel='self']/@href", ns) { + string("http://localhost:8080/people") + } + } +---- +====== + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/filters.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/filters.adoc new file mode 100644 index 000000000000..5060f27c3d6e --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/filters.adoc @@ -0,0 +1,28 @@ +[[mockmvc-server-filters]] += Filter Registrations +:page-section-summary-toc: 1 + +When setting up a `MockMvc` instance, you can register one or more Servlet `Filter` +instances, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed +---- +====== + +Registered filters are invoked through the `MockFilterChain` from `spring-test`, and the +last filter delegates to the `DispatcherServlet`. + + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/requests.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/requests.adoc new file mode 100644 index 000000000000..a7b565abddda --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/requests.adoc @@ -0,0 +1,170 @@ +[[mockmvc-server-performing-requests]] += Performing Requests + +This section shows how to use MockMvc on its own to perform requests and verify responses. +If using MockMvc through the `WebTestClient` please see the corresponding section on +xref:testing/webtestclient.adoc#webtestclient-tests[Writing Tests] instead. + +To perform requests that use any HTTP method, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // static import of MockMvcRequestBuilders.* + + mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON)); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.test.web.servlet.post + + mockMvc.post("/hotels/{id}", 42) { + accept = MediaType.APPLICATION_JSON + } +---- +====== + +You can also perform file upload requests that internally use +`MockMultipartHttpServletRequest` so that there is no actual parsing of a multipart +request. Rather, you have to set it up to be similar to the following example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8"))); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.test.web.servlet.multipart + + mockMvc.multipart("/doc") { + file("a1", "ABC".toByteArray(charset("UTF8"))) + } +---- +====== + +You can specify query parameters in URI template style, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + mockMvc.perform(get("/hotels?thing={thing}", "somewhere")); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + mockMvc.get("/hotels?thing={thing}", "somewhere") +---- +====== + +You can also add Servlet request parameters that represent either query or form +parameters, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + mockMvc.perform(get("/hotels").param("thing", "somewhere")); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.test.web.servlet.get + + mockMvc.get("/hotels") { + param("thing", "somewhere") + } +---- +====== + +If application code relies on Servlet request parameters and does not check the query +string explicitly (as is most often the case), it does not matter which option you use. +Keep in mind, however, that query parameters provided with the URI template are decoded +while request parameters provided through the `param(...)` method are expected to already +be decoded. + +In most cases, it is preferable to leave the context path and the Servlet path out of the +request URI. If you must test with the full request URI, be sure to set the `contextPath` +and `servletPath` accordingly so that request mappings work, as the following example +shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main")) +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + import org.springframework.test.web.servlet.get + + mockMvc.get("/app/main/hotels/{id}") { + contextPath = "/app" + servletPath = "/main" + } +---- +====== + +In the preceding example, it would be cumbersome to set the `contextPath` and +`servletPath` with every performed request. Instead, you can set up default request +properties, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + class MyWebTests { + + MockMvc mockMvc; + + @BeforeEach + void setup() { + mockMvc = standaloneSetup(new AccountController()) + .defaultRequest(get("/") + .contextPath("/app").servletPath("/main") + .accept(MediaType.APPLICATION_JSON)).build(); + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed +---- +====== + +The preceding properties affect every request performed through the `MockMvc` instance. +If the same property is also specified on a given request, it overrides the default +value. That is why the HTTP method and URI in the default request do not matter, since +they must be specified on every request. + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/setup-steps.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/setup-steps.adoc new file mode 100644 index 000000000000..ab426c43f332 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/setup-steps.adoc @@ -0,0 +1,63 @@ +[[mockmvc-server-setup-steps]] += Setup Features + +No matter which MockMvc builder you use, all `MockMvcBuilder` implementations provide +some common and very useful features. For example, you can declare an `Accept` header for +all requests and expect a status of 200 as well as a `Content-Type` header in all +responses, as follows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // static import of MockMvcBuilders.standaloneSetup + + MockMvc mockMvc = standaloneSetup(new MusicController()) + .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON)) + .alwaysExpect(status().isOk()) + .alwaysExpect(content().contentType("application/json;charset=UTF-8")) + .build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed +---- +====== + +In addition, third-party frameworks (and applications) can pre-package setup +instructions, such as those in a `MockMvcConfigurer`. The Spring Framework has one such +built-in implementation that helps to save and re-use the HTTP session across requests. +You can use it as follows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // static import of SharedHttpSessionConfigurer.sharedHttpSession + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController()) + .apply(sharedHttpSession()) + .build(); + + // Use mockMvc to perform requests... +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed +---- +====== + +See the javadoc for +{spring-framework-api}/test/web/servlet/setup/ConfigurableMockMvcBuilder.html[`ConfigurableMockMvcBuilder`] +for a list of all MockMvc builder features or use the IDE to explore the available options. + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/setup.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/setup.adoc new file mode 100644 index 000000000000..1e87ae7c9150 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/setup.adoc @@ -0,0 +1,100 @@ +[[mockmvc-setup]] += Configuring MockMvc + +MockMvc can be setup in one of two ways. One is to point directly to the controllers you +want to test and programmatically configure Spring MVC infrastructure. The second is to +point to Spring configuration with Spring MVC and controller infrastructure in it. + +TIP: For a comparison of those two modes, check xref:testing/mockmvc/setup-options.adoc[Setup Options]. + +To set up MockMvc for testing a specific controller, use the following: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + class MyWebTests { + + MockMvc mockMvc; + + @BeforeEach + void setup() { + this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build(); + } + + // ... + + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + class MyWebTests { + + lateinit var mockMvc : MockMvc + + @BeforeEach + fun setup() { + mockMvc = MockMvcBuilders.standaloneSetup(AccountController()).build() + } + + // ... + + } +---- +====== + +Or you can also use this setup when testing through the +xref:testing/webtestclient.adoc#webtestclient-controller-config[WebTestClient] which delegates to the same builder +as shown above. + +To set up MockMvc through Spring configuration, use the following: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitWebConfig(locations = "my-servlet-context.xml") + class MyWebTests { + + MockMvc mockMvc; + + @BeforeEach + void setup(WebApplicationContext wac) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + // ... + + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitWebConfig(locations = ["my-servlet-context.xml"]) + class MyWebTests { + + lateinit var mockMvc: MockMvc + + @BeforeEach + fun setup(wac: WebApplicationContext) { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build() + } + + // ... + + } +---- +====== + +Or you can also use this setup when testing through the +xref:testing/webtestclient.adoc#webtestclient-context-config[WebTestClient] which delegates to the same builder +as shown above. diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/static-imports.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/static-imports.adoc new file mode 100644 index 000000000000..652eef58b31b --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/static-imports.adoc @@ -0,0 +1,18 @@ +[[mockmvc-server-static-imports]] += Static Imports +:page-section-summary-toc: 1 + +When using MockMvc directly to perform requests, you'll need static imports for: + +- `MockMvcBuilders.{asterisk}` +- `MockMvcRequestBuilders.{asterisk}` +- `MockMvcResultMatchers.{asterisk}` +- `MockMvcResultHandlers.{asterisk}` + +An easy way to remember that is search for `MockMvc*`. If using Eclipse be sure to also +add the above as "`favorite static members`" in the Eclipse preferences. + +When using MockMvc through the xref:testing/webtestclient.adoc[WebTestClient] you do not need static imports. +The `WebTestClient` provides a fluent API without static imports. + + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/vs-streaming-response.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/vs-streaming-response.adoc new file mode 100644 index 000000000000..18813c7c529e --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/vs-streaming-response.adoc @@ -0,0 +1,16 @@ +[[mockmvc-vs-streaming-response]] += Streaming Responses + +You can use `WebTestClient` to test xref:testing/webtestclient.adoc#webtestclient-stream[streaming responses] +such as Server-Sent Events. However, `MockMvcWebTestClient` doesn't support infinite +streams because there is no way to cancel the server stream from the client side. +To test infinite streams, you'll need to +xref:testing/webtestclient.adoc#webtestclient-server-config[bind to] a running server, +or when using Spring Boot, +{spring-boot-docs-ref}/testing/spring-boot-applications.html#testing.spring-boot-applications.with-running-server[test with a running server]. + +`MockMvcWebTestClient` does support asynchronous responses, and even streaming responses. +The limitation is that it can't influence the server to stop, and therefore the server +must finish writing the response on its own. + + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit.adoc new file mode 100644 index 000000000000..52cbacc3e797 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit.adoc @@ -0,0 +1,21 @@ +[[mockmvc-server-htmlunit]] += HtmlUnit Integration +:page-section-summary-toc: 1 + +Spring provides integration between xref:testing/mockmvc/overview.adoc[MockMvc] and +https://htmlunit.sourceforge.io/[HtmlUnit]. This simplifies performing end-to-end testing +when using HTML-based views. This integration lets you: + +* Easily test HTML pages by using tools such as + https://htmlunit.sourceforge.io/[HtmlUnit], + https://www.seleniumhq.org[WebDriver], and + https://www.gebish.org/manual/current/#spock-junit-testng[Geb] without the need to + deploy to a Servlet container. +* Test JavaScript within pages. +* Optionally, test using mock services to speed up testing. +* Share logic between in-container end-to-end tests and out-of-container integration tests. + +NOTE: MockMvc works with templating technologies that do not rely on a Servlet Container +(for example, Thymeleaf, FreeMarker, and others), but it does not work with JSPs, since +they rely on the Servlet container. + diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/geb.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc similarity index 84% rename from framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/geb.adoc rename to framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc index 8be8dd529290..ad4ece55138b 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/geb.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc @@ -1,18 +1,18 @@ -[[spring-mvc-test-server-htmlunit-geb]] +[[mockmvc-server-htmlunit-geb]] = MockMvc and Geb In the previous section, we saw how to use MockMvc with WebDriver. In this section, we use https://www.gebish.org/[Geb] to make our tests even Groovy-er. -[[spring-mvc-test-server-htmlunit-geb-why]] +[[mockmvc-server-htmlunit-geb-why]] == Why Geb and MockMvc? Geb is backed by WebDriver, so it offers many of the -xref:testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[same benefits] that we get from +xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[same benefits] that we get from WebDriver. However, Geb makes things even easier by taking care of some of the boilerplate code for us. -[[spring-mvc-test-server-htmlunit-geb-setup]] +[[mockmvc-server-htmlunit-geb-setup]] == MockMvc and Geb Setup We can easily initialize a Geb `Browser` with a Selenium WebDriver that uses MockMvc, as @@ -28,14 +28,14 @@ def setup() { ---- NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced -usage, see xref:testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`]. +usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`]. This ensures that any URL referencing `localhost` as the server is directed to our `MockMvc` instance without the need for a real HTTP connection. Any other URL is requested by using a network connection as normal. This lets us easily test the use of CDNs. -[[spring-mvc-test-server-htmlunit-geb-usage]] +[[mockmvc-server-htmlunit-geb-usage]] == MockMvc and Geb Usage Now we can use Geb as we normally would but without the need to deploy our application to @@ -62,7 +62,7 @@ forwarded to the current page object. This removes a lot of the boilerplate code needed when using WebDriver directly. As with direct WebDriver usage, this improves on the design of our -xref:testing/spring-mvc-test-framework/server-htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object +xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object Pattern. As mentioned previously, we can use the Page Object Pattern with HtmlUnit and WebDriver, but it is even easier with Geb. Consider our new Groovy-based `CreateMessagePage` implementation: diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc similarity index 78% rename from framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc rename to framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc index 145fbb6b809d..97dd4171b956 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc @@ -1,15 +1,14 @@ -[[spring-mvc-test-server-htmlunit-mah]] +[[mockmvc-server-htmlunit-mah]] = MockMvc and HtmlUnit This section describes how to integrate MockMvc and HtmlUnit. Use this option if you want to use the raw HtmlUnit libraries. -[[spring-mvc-test-server-htmlunit-mah-setup]] +[[mockmvc-server-htmlunit-mah-setup]] == MockMvc and HtmlUnit Setup First, make sure that you have included a test dependency on -`net.sourceforge.htmlunit:htmlunit`. In order to use HtmlUnit with Apache HttpComponents -4.5+, you need to use HtmlUnit 2.18 or higher. +`org.htmlunit:htmlunit`. We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the `MockMvcWebClientBuilder`, as follows: @@ -18,7 +17,7 @@ We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by usi ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient webClient; @@ -32,7 +31,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- lateinit var webClient: WebClient @@ -46,14 +45,14 @@ Kotlin:: ====== NOTE: This is a simple example of using `MockMvcWebClientBuilder`. For advanced usage, -see xref:testing/spring-mvc-test-framework/server-htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`]. +see xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`]. This ensures that any URL that references `localhost` as the server is directed to our `MockMvc` instance without the need for a real HTTP connection. Any other URL is requested by using a network connection, as normal. This lets us easily test the use of CDNs. -[[spring-mvc-test-server-htmlunit-mah-usage]] +[[mockmvc-server-htmlunit-mah-usage]] == MockMvc and HtmlUnit Usage Now we can use HtmlUnit as we normally would but without the need to deploy our @@ -64,21 +63,21 @@ message with the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form"); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val createMsgFormPage = webClient.getPage("http://localhost/messages/form") ---- ====== NOTE: The default context path is `""`. Alternatively, we can specify the context path, -as described in xref:testing/spring-mvc-test-framework/server-htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`]. +as described in xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`]. Once we have a reference to the `HtmlPage`, we can then fill out the form and submit it to create a message, as the following example shows: @@ -87,7 +86,7 @@ to create a message, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm"); HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary"); @@ -100,7 +99,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val form = createMsgFormPage.getHtmlElementById("messageForm") val summaryInput = createMsgFormPage.getHtmlElementById("summary") @@ -113,13 +112,13 @@ Kotlin:: ====== Finally, we can verify that a new message was created successfully. The following -assertions use the https://assertj.github.io/doc/[AssertJ] library: +assertions use the {assertj-docs}[AssertJ] library: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123"); String id = newMessagePage.getHtmlElementById("id").getTextContent(); @@ -132,7 +131,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123") val id = newMessagePage.getHtmlElementById("id").getTextContent() @@ -145,7 +144,7 @@ Kotlin:: ====== The preceding code improves on our -xref:testing/spring-mvc-test-framework/server-htmlunit/why.adoc#spring-mvc-test-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways. +xref:testing/mockmvc/htmlunit/why.adoc#spring-mvc-test-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways. First, we no longer have to explicitly verify our form and then create a request that looks like the form. Instead, we request the form, fill it out, and submit it, thereby significantly reducing the overhead. @@ -157,7 +156,7 @@ the behavior of JavaScript within our pages. See the https://htmlunit.sourceforge.io/gettingStarted.html[HtmlUnit documentation] for additional information about using HtmlUnit. -[[spring-mvc-test-server-htmlunit-mah-advanced-builder]] +[[mockmvc-server-htmlunit-mah-advanced-builder]] == Advanced `MockMvcWebClientBuilder` In the examples so far, we have used `MockMvcWebClientBuilder` in the simplest way @@ -168,7 +167,7 @@ the Spring TestContext Framework. This approach is repeated in the following exa ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient webClient; @@ -182,7 +181,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- lateinit var webClient: WebClient @@ -201,7 +200,7 @@ We can also specify additional configuration options, as the following example s ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient webClient; @@ -221,7 +220,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- lateinit var webClient: WebClient @@ -247,7 +246,7 @@ instance separately and supplying it to the `MockMvcWebClientBuilder`, as follow ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- MockMvc mockMvc = MockMvcBuilders .webAppContextSetup(context) @@ -266,9 +265,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed ---- ====== @@ -276,5 +275,5 @@ This is more verbose, but, by building the `WebClient` with a `MockMvc` instance the full power of MockMvc at our fingertips. TIP: For additional information on creating a `MockMvc` instance, see -xref:testing/spring-mvc-test-framework/server-setup-options.adoc[Setup Choices]. +xref:testing/mockmvc/hamcrest/setup.adoc[Configuring MockMvc]. diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc similarity index 83% rename from framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc rename to framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc index 392ae27a8cbd..175af0f55eec 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc @@ -1,11 +1,11 @@ -[[spring-mvc-test-server-htmlunit-webdriver]] +[[mockmvc-server-htmlunit-webdriver]] = MockMvc and WebDriver In the previous sections, we have seen how to use MockMvc in conjunction with the raw HtmlUnit APIs. In this section, we use additional abstractions within the Selenium https://docs.seleniumhq.org/projects/webdriver/[WebDriver] to make things even easier. -[[spring-mvc-test-server-htmlunit-webdriver-why]] +[[mockmvc-server-htmlunit-webdriver-why]] == Why WebDriver and MockMvc? We can already use HtmlUnit and MockMvc, so why would we want to use WebDriver? The @@ -30,7 +30,7 @@ following repeated in multiple places within our tests: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary"); summaryInput.setValueAttribute(summary); @@ -38,7 +38,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val summaryInput = currentPage.getHtmlElementById("summary") summaryInput.setValueAttribute(summary) @@ -53,7 +53,7 @@ ideally extract this code into its own method, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) { setSummary(currentPage, summary); @@ -68,7 +68,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{ setSummary(currentPage, summary); @@ -91,7 +91,7 @@ represents the `HtmlPage` we are currently on, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class CreateMessagePage { @@ -128,7 +128,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CreateMessagePage(private val currentPage: HtmlPage) { @@ -162,11 +162,11 @@ https://github.com/SeleniumHQ/selenium/wiki/PageObjects[Page Object Pattern]. Wh can certainly do this with HtmlUnit, WebDriver provides some tools that we explore in the following sections to make this pattern much easier to implement. -[[spring-mvc-test-server-htmlunit-webdriver-setup]] +[[mockmvc-server-htmlunit-webdriver-setup]] == MockMvc and WebDriver Setup -To use Selenium WebDriver with the Spring MVC Test framework, make sure that your project -includes a test dependency on `org.seleniumhq.selenium:selenium-htmlunit-driver`. +To use Selenium WebDriver with `MockMvc`, make sure that your project includes a test +dependency on `org.seleniumhq.selenium:selenium-htmlunit3-driver`. We can easily create a Selenium WebDriver that integrates with MockMvc by using the `MockMvcHtmlUnitDriverBuilder` as the following example shows: @@ -175,7 +175,7 @@ We can easily create a Selenium WebDriver that integrates with MockMvc by using ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebDriver driver; @@ -189,7 +189,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- lateinit var driver: WebDriver @@ -203,14 +203,14 @@ Kotlin:: ====== NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced -usage, see xref:testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`]. +usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`]. The preceding example ensures that any URL that references `localhost` as the server is directed to our `MockMvc` instance without the need for a real HTTP connection. Any other URL is requested by using a network connection, as normal. This lets us easily test the use of CDNs. -[[spring-mvc-test-server-htmlunit-webdriver-usage]] +[[mockmvc-server-htmlunit-webdriver-usage]] == MockMvc and WebDriver Usage Now we can use WebDriver as we normally would but without the need to deploy our @@ -222,14 +222,14 @@ message with the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- CreateMessagePage page = CreateMessagePage.to(driver); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val page = CreateMessagePage.to(driver) ---- @@ -243,7 +243,7 @@ We can then fill out the form and submit it to create a message, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ViewMessagePage viewMessagePage = page.createMessage(ViewMessagePage.class, expectedSummary, expectedText); @@ -251,7 +251,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val viewMessagePage = page.createMessage(ViewMessagePage::class, expectedSummary, expectedText) @@ -259,9 +259,9 @@ Kotlin:: ====== -- -This improves on the design of our xref:testing/spring-mvc-test-framework/server-htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] +This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] by leveraging the Page Object Pattern. As we mentioned in -xref:testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern +xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern with HtmlUnit, but it is much easier with WebDriver. Consider the following `CreateMessagePage` implementation: @@ -270,7 +270,7 @@ with HtmlUnit, but it is much easier with WebDriver. Consider the following ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class CreateMessagePage extends AbstractPage { // <1> @@ -317,7 +317,7 @@ annotation to look up our submit button with a `css` selector (`input[type=submi Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { // <1> @@ -362,14 +362,14 @@ annotation to look up our submit button with a `css` selector (*input[type=submi -- Finally, we can verify that a new message was created successfully. The following -assertions use the https://assertj.github.io/doc/[AssertJ] assertion library: +assertions use the {assertj-docs}[AssertJ] assertion library: -- [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage); assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message"); @@ -377,7 +377,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- assertThat(viewMessagePage.message).isEqualTo(expectedMessage) assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message") @@ -393,7 +393,7 @@ example, it exposes a method that returns a `Message` object: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public Message getMessage() throws ParseException { Message message = new Message(); @@ -407,7 +407,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun getMessage() = Message(getId(), getCreated(), getSummary(), getText()) ---- @@ -424,7 +424,7 @@ as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @AfterEach void destroy() { @@ -436,7 +436,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @AfterEach fun destroy() { @@ -451,7 +451,7 @@ Kotlin:: For additional information on using WebDriver, see the Selenium https://github.com/SeleniumHQ/selenium/wiki/Getting-Started[WebDriver documentation]. -[[spring-mvc-test-server-htmlunit-webdriver-advanced-builder]] +[[mockmvc-server-htmlunit-webdriver-advanced-builder]] == Advanced `MockMvcHtmlUnitDriverBuilder` In the examples so far, we have used `MockMvcHtmlUnitDriverBuilder` in the simplest way @@ -462,7 +462,7 @@ the Spring TestContext Framework. This approach is repeated here, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebDriver driver; @@ -476,7 +476,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- lateinit var driver: WebDriver @@ -495,7 +495,7 @@ We can also specify additional configuration options, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebDriver driver; @@ -515,7 +515,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- lateinit var driver: WebDriver @@ -541,7 +541,7 @@ instance separately and supplying it to the `MockMvcHtmlUnitDriverBuilder`, as f ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- MockMvc mockMvc = MockMvcBuilders .webAppContextSetup(context) @@ -560,9 +560,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed + // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed ---- ====== @@ -570,5 +570,5 @@ This is more verbose, but, by building the `WebDriver` with a `MockMvc` instance the full power of MockMvc at our fingertips. TIP: For additional information on creating a `MockMvc` instance, see -xref:testing/spring-mvc-test-framework/server-setup-options.adoc[Setup Choices]. +xref:testing/mockmvc/hamcrest/setup.adoc[Configuring MockMvc]. diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/why.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/why.adoc similarity index 86% rename from framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/why.adoc rename to framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/why.adoc index 04493364f859..710a310f1d5d 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/why.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/why.adoc @@ -1,4 +1,4 @@ -[[spring-mvc-test-server-htmlunit-why]] +[[mockmvc-server-htmlunit-why]] = Why HtmlUnit Integration? The most obvious question that comes to mind is "`Why do I need this?`" The answer is @@ -12,7 +12,7 @@ With Spring MVC Test, we can easily test if we are able to create a `Message`, a ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- MockHttpServletRequestBuilder createMessage = post("/messages/") .param("summary", "Spring Rocks") @@ -25,7 +25,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test fun test() { @@ -67,7 +67,7 @@ naive attempt might resemble the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- mockMvc.perform(get("/messages/form")) .andExpect(xpath("//input[@name='summary']").exists()) @@ -76,7 +76,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- mockMvc.get("/messages/form").andExpect { xpath("//input[@name='summary']") { exists() } @@ -87,15 +87,15 @@ Kotlin:: This test has some obvious drawbacks. If we update our controller to use the parameter `message` instead of `text`, our form test continues to pass, even though the HTML form -is out of synch with the controller. To resolve this we can combine our two tests, as +is out of sync with the controller. To resolve this we can combine our two tests, as follows: [tabs] ====== Java:: + -[[spring-mvc-test-server-htmlunit-mock-mvc-test]] -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[[mockmvc-server-htmlunit-mock-mvc-test]] +[source,java,indent=0,subs="verbatim,quotes"] ---- String summaryParamName = "summary"; String textParamName = "text"; @@ -114,7 +114,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val summaryParamName = "summary"; val textParamName = "text"; @@ -151,7 +151,7 @@ the input to a user for creating a message. In addition, our form view can poten use additional resources that impact the behavior of the page, such as JavaScript validation. -[[spring-mvc-test-server-htmlunit-why-integration]] +[[mockmvc-server-htmlunit-why-integration]] == Integration Testing to the Rescue? To resolve the issues mentioned earlier, we could perform end-to-end integration testing, @@ -181,23 +181,23 @@ and without side effects. We can then implement a small number of true end-to-en integration tests that validate simple workflows to ensure that everything works together properly. -[[spring-mvc-test-server-htmlunit-why-mockmvc]] +[[mockmvc-server-htmlunit-why-mockmvc]] == Enter HtmlUnit Integration So how can we achieve a balance between testing the interactions of our pages and still retain good performance within our test suite? The answer is: "`By integrating MockMvc with HtmlUnit.`" -[[spring-mvc-test-server-htmlunit-options]] +[[mockmvc-server-htmlunit-options]] == HtmlUnit Integration Options You have a number of options when you want to integrate MockMvc with HtmlUnit: -* xref:testing/spring-mvc-test-framework/server-htmlunit/mah.adoc[MockMvc and HtmlUnit]: Use this option if you +* xref:testing/mockmvc/htmlunit/mah.adoc[MockMvc and HtmlUnit]: Use this option if you want to use the raw HtmlUnit libraries. -* xref:testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc[MockMvc and WebDriver]: Use this option to +* xref:testing/mockmvc/htmlunit/webdriver.adoc[MockMvc and WebDriver]: Use this option to ease development and reuse code between integration and end-to-end testing. -* xref:testing/spring-mvc-test-framework/server-htmlunit/geb.adoc[MockMvc and Geb]: Use this option if you want to +* xref:testing/mockmvc/htmlunit/geb.adoc[MockMvc and Geb]: Use this option if you want to use Groovy for testing, ease development, and reuse code between integration and end-to-end testing. diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/overview.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/overview.adoc new file mode 100644 index 000000000000..a1571c1bd938 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/overview.adoc @@ -0,0 +1,24 @@ +[[mockmvc-overview]] += Overview +:page-section-summary-toc: 1 + +You can write plain unit tests for Spring MVC by instantiating a controller, injecting it +with dependencies, and calling its methods. However such tests do not verify request +mappings, data binding, message conversion, type conversion, or validation and also do +not involve any of the supporting `@InitBinder`, `@ModelAttribute`, or +`@ExceptionHandler` methods. + +`MockMvc` aims to provide more complete testing support for Spring MVC controllers +without a running server. It does that by invoking the `DispatcherServlet` and passing +xref:testing/unit.adoc#mock-objects-servlet["mock" implementations of the Servlet API] +from the `spring-test` module which replicates the full Spring MVC request handling +without a running server. + +MockMvc is a server-side test framework that lets you verify most of the functionality of +a Spring MVC application using lightweight and targeted tests. You can use it on its own +to perform requests and to verify responses using Hamcrest or through `MockMvcTester` +which provides a fluent API using AssertJ. You can also use it through the +xref:testing/webtestclient.adoc[WebTestClient] API with MockMvc plugged in as the server +to handle requests. + + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/resources.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/resources.adoc new file mode 100644 index 000000000000..f749cead6f09 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/resources.adoc @@ -0,0 +1,11 @@ +[[mockmvc-server-resources]] += Further Examples +:page-section-summary-toc: 1 + +The framework's own test suite includes +{spring-framework-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples[ +many sample tests] intended to show how to use MockMvc on its own or through the +{spring-framework-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client[ +WebTestClient]. Browse these examples for further ideas. + + diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/setup-options.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/setup-options.adoc new file mode 100644 index 000000000000..5a12a668c59c --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/setup-options.adoc @@ -0,0 +1,32 @@ +[[mockmvc-server-setup-options]] += Setup Options + +MockMvc can be set up in one of two ways. + +`WebApplicationContext` :: + Point to Spring configuration with Spring MVC and controller infrastructure in it. +Standalone :: + Point directly to the controllers you want to test and programmatically configure Spring + MVC infrastructure. + +Which setup option should you use? + +A `WebApplicationContext`-based test loads your actual Spring MVC configuration, +resulting in a more complete integration test. Since the TestContext framework caches the +loaded Spring configuration, it helps keep tests running fast, even as you introduce more +tests in your test suite using the same configuration. Furthermore, you can override +services used by your controller using `@MockitoBean` or `@TestBean` to remain focused on +testing the web layer. + +A standalone test, on the other hand, is a little closer to a unit test. It tests one +controller at a time. You can manually inject the controller with mock dependencies, and +it does not involve loading Spring configuration. Such tests are more focused on style +and make it easier to see which controller is being tested, whether any specific Spring +MVC configuration is required to work, and so on. The standalone setup is also a very +convenient way to write ad-hoc tests to verify specific behavior or to debug an issue. + +As with most "integration versus unit testing" debates, there is no right or wrong +answer. However, using standalone tests does imply the need for additional integration +tests to verify your Spring MVC configuration. Alternatively, you can write all your +tests with a `WebApplicationContext`, so that they always test against your actual Spring +MVC configuration. diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/vs-end-to-end-integration-tests.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/vs-end-to-end-integration-tests.adoc new file mode 100644 index 000000000000..4a4e2eee7a72 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/vs-end-to-end-integration-tests.adoc @@ -0,0 +1,46 @@ +[[mockmvc-vs-end-to-end-integration-tests]] += MockMvc vs End-to-End Tests + +MockMvc is built on Servlet API mock implementations from the +`spring-test` module and does not rely on a running container. Therefore, there are +some differences when compared to full end-to-end integration tests with an actual +client and a live server running. + +The easiest way to think about this is by starting with a blank `MockHttpServletRequest`. +Whatever you add to it is what the request becomes. Things that may catch you by surprise +are that there is no context path by default; no `jsessionid` cookie; no forwarding, +error, or async dispatches; and, therefore, no actual JSP rendering. Instead, +"forwarded" and "redirected" URLs are saved in the `MockHttpServletResponse` and can +be asserted with expectations. + +This means that, if you use JSPs, you can verify the JSP page to which the request was +forwarded, but no HTML is rendered. In other words, the JSP is not invoked. Note, +however, that all other rendering technologies which do not rely on forwarding, such as +Thymeleaf and Freemarker, render HTML to the response body as expected. The same is true +for rendering JSON, XML, and other formats through `@ResponseBody` methods. + +Alternatively, you may consider the full end-to-end integration testing support from +Spring Boot with `@SpringBootTest`. See the +{spring-boot-docs-ref}/testing/spring-boot-applications.html[Spring Boot Reference Guide]. + +There are pros and cons for each approach. The options provided in Spring MVC Test are +different stops on the scale from classic unit testing to full integration testing. To be +certain, none of the options in Spring MVC Test fall under the category of classic unit +testing, but they are a little closer to it. For example, you can isolate the web layer +by injecting mocked services into controllers, in which case you are testing the web +layer only through the `DispatcherServlet` but with actual Spring configuration, as you +might test the data access layer in isolation from the layers above it. Also, you can use +the standalone setup, focusing on one controller at a time and manually providing the +configuration required to make it work. + +Another important distinction when using Spring MVC Test is that, conceptually, such +tests are server-side tests, so you can check what handler was used, if an exception was +handled with a `HandlerExceptionResolver`, what the content of the model is, what binding +errors there were, and other details. That means that it is easier to write expectations, +since the server is not an opaque box, as it is when testing it through an actual HTTP +client. This is generally an advantage of classic unit testing: it is easier to write, +reason about, and debug but does not replace the need for full integration tests. At the +same time, it is important not to lose sight of the fact that the response is the most +important thing to check. In short, there is room for multiple styles and strategies +of testing even within the same project. + diff --git a/framework-docs/modules/ROOT/pages/testing/resources.adoc b/framework-docs/modules/ROOT/pages/testing/resources.adoc index c7b3c247dc10..63c488057edc 100644 --- a/framework-docs/modules/ROOT/pages/testing/resources.adoc +++ b/framework-docs/modules/ROOT/pages/testing/resources.adoc @@ -2,31 +2,38 @@ = Further Resources See the following resources for more information about testing: -* https://www.junit.org/[JUnit]: "A programmer-friendly testing framework for Java and the JVM". - Used by the Spring Framework in its test suite and supported in the +https://www.junit.org/[JUnit] :: + "A programmer-friendly testing framework for Java and the JVM". Used by the Spring + Framework in its test suite and supported in the xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. -* https://testng.org/[TestNG]: A testing framework inspired by JUnit with added support - for test groups, data-driven testing, distributed testing, and other features. Supported - in the xref:testing/testcontext-framework.adoc[Spring TestContext Framework] -* https://assertj.github.io/doc/[AssertJ]: "Fluent assertions for Java", - including support for Java 8 lambdas, streams, and numerous other features. -* https://en.wikipedia.org/wiki/Mock_Object[Mock Objects]: Article in Wikipedia. -* http://www.mockobjects.com/[MockObjects.com]: Web site dedicated to mock objects, a - technique for improving the design of code within test-driven development. -* https://mockito.github.io[Mockito]: Java mock library based on the - http://xunitpatterns.com/Test%20Spy.html[Test Spy] pattern. Used by the Spring Framework - in its test suite. -* https://easymock.org/[EasyMock]: Java library "that provides Mock Objects for - interfaces (and objects through the class extension) by generating them on the fly using - Java's proxy mechanism." -* https://jmock.org/[JMock]: Library that supports test-driven development of Java code - with mock objects. -* https://www.dbunit.org/[DbUnit]: JUnit extension (also usable with Ant and Maven) that - is targeted at database-driven projects and, among other things, puts your database into - a known state between test runs. -* https://www.testcontainers.org/[Testcontainers]: Java library that supports JUnit - tests, providing lightweight, throwaway instances of common databases, Selenium web - browsers, or anything else that can run in a Docker container. -* https://sourceforge.net/projects/grinder/[The Grinder]: Java load testing framework. -* https://github.com/Ninja-Squad/springmockk[SpringMockK]: Support for Spring Boot - integration tests written in Kotlin using https://mockk.io/[MockK] instead of Mockito. +https://testng.org/[TestNG] :: + A testing framework inspired by JUnit with added support for test groups, data-driven + testing, distributed testing, and other features. Supported in the + xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. +{assertj-docs}[AssertJ] :: + "Fluent assertions for Java", including support for Java 8 lambdas, streams, and + numerous other features. Supported in Spring's + xref:testing/mockmvc/assertj.adoc[MockMvc testing support]. +https://en.wikipedia.org/wiki/Mock_Object[Mock Objects] :: + Article in Wikipedia. +https://site.mockito.org[Mockito] :: + Java mock library based on the http://xunitpatterns.com/Test%20Spy.html[Test Spy] + pattern. Used by the Spring Framework in its test suite. +https://easymock.org/[EasyMock] :: + Java library "that provides Mock Objects for interfaces (and objects through the class + extension) by generating them on the fly using Java's proxy mechanism." +https://jmock.org/[JMock] :: + Library that supports test-driven development of Java code with mock objects. +https://www.dbunit.org/[DbUnit] :: + JUnit extension (also usable with Ant and Maven) that is targeted at database-driven + projects and, among other things, puts your database into a known state between test + runs. +{testcontainers-site}[Testcontainers] :: + Java library that supports JUnit tests, providing lightweight, throwaway instances of + common databases, Selenium web browsers, or anything else that can run in a Docker + container. +https://sourceforge.net/projects/grinder/[The Grinder] :: + Java load testing framework. +https://github.com/Ninja-Squad/springmockk[SpringMockK] :: + Support for Spring Boot integration tests written in Kotlin using + https://mockk.io/[MockK] instead of Mockito. 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 a223a9f4ca35..55738c214f7d 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 @@ -10,7 +10,7 @@ example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RestTemplate restTemplate = new RestTemplate(); @@ -24,7 +24,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val restTemplate = RestTemplate() @@ -55,14 +55,14 @@ requests are allowed to come in any order. The following example uses `ignoreExp ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build(); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build() ---- @@ -77,7 +77,7 @@ argument that specifies a count range (for example, `once`, `manyTimes`, `max`, ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RestTemplate restTemplate = new RestTemplate(); @@ -92,7 +92,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val restTemplate = RestTemplate() @@ -122,7 +122,7 @@ logic but without running a server. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc)); @@ -132,7 +132,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build() restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc)) @@ -149,7 +149,7 @@ of mocking the response. The following example shows how to do that through ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RestTemplate restTemplate = new RestTemplate(); @@ -167,7 +167,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val restTemplate = RestTemplate() @@ -193,7 +193,7 @@ Then we define expectations with two kinds of responses: * a response obtained through a call to the `/quoteOfTheDay` endpoint In the second case, the request is executed through the `ClientHttpRequestFactory` that was -captured earlier. This generates a response that could e.g. come from an actual remote server, +captured earlier. This generates a response that could, for example, come from an actual remote server, depending on how the `RestTemplate` was originally configured. [[spring-mvc-test-client-static-imports]] @@ -211,5 +211,5 @@ configuration. Check for the support for code completion on static members. == Further Examples of Client-side REST Tests Spring MVC Test's own tests include -{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/client/samples[example +{spring-framework-code}/spring-test/src/test/java/org/springframework/test/web/client/samples[example tests] of client-side REST tests. diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework.adoc deleted file mode 100644 index ec1900709d29..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework.adoc +++ /dev/null @@ -1,15 +0,0 @@ -[[spring-mvc-test-framework]] -= MockMvc -:page-section-summary-toc: 1 - -The Spring MVC Test framework, also known as MockMvc, provides support for testing Spring -MVC applications. It performs full Spring MVC request handling but via mock request and -response objects instead of a running server. - -MockMvc can be used on its own to perform requests and verify responses. It can also be -used through the xref:testing/webtestclient.adoc[WebTestClient] where MockMvc is plugged in as the server to handle -requests with. The advantage of `WebTestClient` is the option to work with higher level -objects instead of raw data as well as the ability to switch to full, end-to-end HTTP -tests against a live server and use the same test API. - - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-defining-expectations.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-defining-expectations.adoc deleted file mode 100644 index e581e32b1905..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-defining-expectations.adoc +++ /dev/null @@ -1,251 +0,0 @@ -[[spring-mvc-test-server-defining-expectations]] -= Defining Expectations - -You can define expectations by appending one or more `andExpect(..)` calls after -performing a request, as the following example shows. As soon as one expectation fails, -no other expectations will be asserted. - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* - - mockMvc.perform(get("/accounts/1")).andExpect(status().isOk()); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.test.web.servlet.get - - mockMvc.get("/accounts/1").andExpect { - status { isOk() } - } ----- -====== - -You can define multiple expectations by appending `andExpectAll(..)` after performing a -request, as the following example shows. In contrast to `andExpect(..)`, -`andExpectAll(..)` guarantees that all supplied expectations will be asserted and that -all failures will be tracked and reported. - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* - - mockMvc.perform(get("/accounts/1")).andExpectAll( - status().isOk(), - content().contentType("application/json;charset=UTF-8")); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.test.web.servlet.get - - mockMvc.get("/accounts/1").andExpectAll { - status { isOk() } - content { contentType(APPLICATION_JSON) } - } ----- -====== - -`MockMvcResultMatchers.*` provides a number of expectations, some of which are further -nested with more detailed expectations. - -Expectations fall in two general categories. The first category of assertions verifies -properties of the response (for example, the response status, headers, and content). -These are the most important results to assert. - -The second category of assertions goes beyond the response. These assertions let you -inspect Spring MVC specific aspects, such as which controller method processed the -request, whether an exception was raised and handled, what the content of the model is, -what view was selected, what flash attributes were added, and so on. They also let you -inspect Servlet specific aspects, such as request and session attributes. - -The following test asserts that binding or validation failed: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - mockMvc.perform(post("/persons")) - .andExpect(status().isOk()) - .andExpect(model().attributeHasErrors("person")); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.test.web.servlet.post - - mockMvc.post("/persons").andExpect { - status { isOk() } - model { - attributeHasErrors("person") - } - } ----- -====== - -Many times, when writing tests, it is useful to dump the results of the performed -request. You can do so as follows, where `print()` is a static import from -`MockMvcResultHandlers`: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - mockMvc.perform(post("/persons")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(model().attributeHasErrors("person")); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.test.web.servlet.post - - mockMvc.post("/persons").andDo { - print() - }.andExpect { - status { isOk() } - model { - attributeHasErrors("person") - } - } ----- -====== - -As long as request processing does not cause an unhandled exception, the `print()` method -prints all the available result data to `System.out`. There is also a `log()` method and -two additional variants of the `print()` method, one that accepts an `OutputStream` and -one that accepts a `Writer`. For example, invoking `print(System.err)` prints the result -data to `System.err`, while invoking `print(myWriter)` prints the result data to a custom -writer. If you want to have the result data logged instead of printed, you can invoke the -`log()` method, which logs the result data as a single `DEBUG` message under the -`org.springframework.test.web.servlet.result` logging category. - -In some cases, you may want to get direct access to the result and verify something that -cannot be verified otherwise. This can be achieved by appending `.andReturn()` after all -other expectations, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn(); - // ... ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - var mvcResult = mockMvc.post("/persons").andExpect { status { isOk() } }.andReturn() - // ... ----- -====== - -If all tests repeat the same expectations, you can set up common expectations once when -building the `MockMvc` instance, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - standaloneSetup(new SimpleController()) - .alwaysExpect(status().isOk()) - .alwaysExpect(content().contentType("application/json;charset=UTF-8")) - .build() ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ----- -====== - -Note that common expectations are always applied and cannot be overridden without -creating a separate `MockMvc` instance. - -When a JSON response content contains hypermedia links created with -https://github.com/spring-projects/spring-hateoas[Spring HATEOAS], you can verify the -resulting links by using JsonPath expressions, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people")); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - mockMvc.get("/people") { - accept(MediaType.APPLICATION_JSON) - }.andExpect { - jsonPath("$.links[?(@.rel == 'self')].href") { - value("http://localhost:8080/people") - } - } ----- -====== - -When XML response content contains hypermedia links created with -https://github.com/spring-projects/spring-hateoas[Spring HATEOAS], you can verify the -resulting links by using XPath expressions: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - Map ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom"); - mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML)) - .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people")); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val ns = mapOf("ns" to "http://www.w3.org/2005/Atom") - mockMvc.get("/handle") { - accept(MediaType.APPLICATION_XML) - }.andExpect { - xpath("/person/ns:link[@rel='self']/@href", ns) { - string("http://localhost:8080/people") - } - } ----- -====== - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-filters.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-filters.adoc deleted file mode 100644 index 7b379b88cde5..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-filters.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[spring-mvc-test-server-filters]] -= Filter Registrations -:page-section-summary-toc: 1 - -When setting up a `MockMvc` instance, you can register one or more Servlet `Filter` -instances, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build(); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ----- -====== - -Registered filters are invoked through the `MockFilterChain` from `spring-test`, and the -last filter delegates to the `DispatcherServlet`. - - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit.adoc deleted file mode 100644 index 03895dfa44e7..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit.adoc +++ /dev/null @@ -1,21 +0,0 @@ -[[spring-mvc-test-server-htmlunit]] -= HtmlUnit Integration -:page-section-summary-toc: 1 - -Spring provides integration between xref:testing/spring-mvc-test-framework/server.adoc[MockMvc] and -https://htmlunit.sourceforge.io/[HtmlUnit]. This simplifies performing end-to-end testing -when using HTML-based views. This integration lets you: - -* Easily test HTML pages by using tools such as - https://htmlunit.sourceforge.io/[HtmlUnit], - https://www.seleniumhq.org[WebDriver], and - https://www.gebish.org/manual/current/#spock-junit-testng[Geb] without the need to - deploy to a Servlet container. -* Test JavaScript within pages. -* Optionally, test using mock services to speed up testing. -* Share logic between in-container end-to-end tests and out-of-container integration tests. - -NOTE: MockMvc works with templating technologies that do not rely on a Servlet Container -(for example, Thymeleaf, FreeMarker, and others), but it does not work with JSPs, since -they rely on the Servlet container. - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-performing-requests.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-performing-requests.adoc deleted file mode 100644 index ba8ac3772dc3..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-performing-requests.adoc +++ /dev/null @@ -1,170 +0,0 @@ -[[spring-mvc-test-server-performing-requests]] -= Performing Requests - -This section shows how to use MockMvc on its own to perform requests and verify responses. -If using MockMvc through the `WebTestClient` please see the corresponding section on -xref:testing/webtestclient.adoc#webtestclient-tests[Writing Tests] instead. - -To perform requests that use any HTTP method, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - // static import of MockMvcRequestBuilders.* - - mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON)); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.test.web.servlet.post - - mockMvc.post("/hotels/{id}", 42) { - accept = MediaType.APPLICATION_JSON - } ----- -====== - -You can also perform file upload requests that internally use -`MockMultipartHttpServletRequest` so that there is no actual parsing of a multipart -request. Rather, you have to set it up to be similar to the following example: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8"))); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.test.web.servlet.multipart - - mockMvc.multipart("/doc") { - file("a1", "ABC".toByteArray(charset("UTF8"))) - } ----- -====== - -You can specify query parameters in URI template style, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - mockMvc.perform(get("/hotels?thing={thing}", "somewhere")); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - mockMvc.get("/hotels?thing={thing}", "somewhere") ----- -====== - -You can also add Servlet request parameters that represent either query or form -parameters, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - mockMvc.perform(get("/hotels").param("thing", "somewhere")); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.test.web.servlet.get - - mockMvc.get("/hotels") { - param("thing", "somewhere") - } ----- -====== - -If application code relies on Servlet request parameters and does not check the query -string explicitly (as is most often the case), it does not matter which option you use. -Keep in mind, however, that query parameters provided with the URI template are decoded -while request parameters provided through the `param(...)` method are expected to already -be decoded. - -In most cases, it is preferable to leave the context path and the Servlet path out of the -request URI. If you must test with the full request URI, be sure to set the `contextPath` -and `servletPath` accordingly so that request mappings work, as the following example -shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main")) ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.test.web.servlet.get - - mockMvc.get("/app/main/hotels/{id}") { - contextPath = "/app" - servletPath = "/main" - } ----- -====== - -In the preceding example, it would be cumbersome to set the `contextPath` and -`servletPath` with every performed request. Instead, you can set up default request -properties, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - class MyWebTests { - - MockMvc mockMvc; - - @BeforeEach - void setup() { - mockMvc = standaloneSetup(new AccountController()) - .defaultRequest(get("/") - .contextPath("/app").servletPath("/main") - .accept(MediaType.APPLICATION_JSON)).build(); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ----- -====== - -The preceding properties affect every request performed through the `MockMvc` instance. -If the same property is also specified on a given request, it overrides the default -value. That is why the HTTP method and URI in the default request do not matter, since -they must be specified on every request. - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-resources.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-resources.adoc deleted file mode 100644 index 7444c1038fb1..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-resources.adoc +++ /dev/null @@ -1,11 +0,0 @@ -[[spring-mvc-test-server-resources]] -= Further Examples -:page-section-summary-toc: 1 - -The framework's own tests include -{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples[ -many sample tests] intended to show how to use MockMvc on its own or through the -{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client[ -WebTestClient]. Browse these examples for further ideas. - - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-options.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-options.adoc deleted file mode 100644 index b38d69ed683e..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-options.adoc +++ /dev/null @@ -1,180 +0,0 @@ -[[spring-mvc-test-server-setup-options]] -= Setup Choices - -MockMvc can be setup in one of two ways. One is to point directly to the controllers you -want to test and programmatically configure Spring MVC infrastructure. The second is to -point to Spring configuration with Spring MVC and controller infrastructure in it. - -To set up MockMvc for testing a specific controller, use the following: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - class MyWebTests { - - MockMvc mockMvc; - - @BeforeEach - void setup() { - this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build(); - } - - // ... - - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class MyWebTests { - - lateinit var mockMvc : MockMvc - - @BeforeEach - fun setup() { - mockMvc = MockMvcBuilders.standaloneSetup(AccountController()).build() - } - - // ... - - } ----- -====== - -Or you can also use this setup when testing through the -xref:testing/webtestclient.adoc#webtestclient-controller-config[WebTestClient] which delegates to the same builder -as shown above. - -To set up MockMvc through Spring configuration, use the following: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @SpringJUnitWebConfig(locations = "my-servlet-context.xml") - class MyWebTests { - - MockMvc mockMvc; - - @BeforeEach - void setup(WebApplicationContext wac) { - this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); - } - - // ... - - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @SpringJUnitWebConfig(locations = ["my-servlet-context.xml"]) - class MyWebTests { - - lateinit var mockMvc: MockMvc - - @BeforeEach - fun setup(wac: WebApplicationContext) { - mockMvc = MockMvcBuilders.webAppContextSetup(wac).build() - } - - // ... - - } ----- -====== - -Or you can also use this setup when testing through the -xref:testing/webtestclient.adoc#webtestclient-context-config[WebTestClient] which delegates to the same builder -as shown above. - - - -Which setup option should you use? - -The `webAppContextSetup` loads your actual Spring MVC configuration, resulting in a more -complete integration test. Since the TestContext framework caches the loaded Spring -configuration, it helps keep tests running fast, even as you introduce more tests in your -test suite. Furthermore, you can inject mock services into controllers through Spring -configuration to remain focused on testing the web layer. The following example declares -a mock service with Mockito: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - ----- - -You can then inject the mock service into the test to set up and verify your -expectations, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @SpringJUnitWebConfig(locations = "test-servlet-context.xml") - class AccountTests { - - @Autowired - AccountService accountService; - - MockMvc mockMvc; - - @BeforeEach - void setup(WebApplicationContext wac) { - this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); - } - - // ... - - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @SpringJUnitWebConfig(locations = ["test-servlet-context.xml"]) - class AccountTests { - - @Autowired - lateinit var accountService: AccountService - - lateinit mockMvc: MockMvc - - @BeforeEach - fun setup(wac: WebApplicationContext) { - mockMvc = MockMvcBuilders.webAppContextSetup(wac).build() - } - - // ... - - } ----- -====== - -The `standaloneSetup`, on the other hand, is a little closer to a unit test. It tests one -controller at a time. You can manually inject the controller with mock dependencies, and -it does not involve loading Spring configuration. Such tests are more focused on style -and make it easier to see which controller is being tested, whether any specific Spring -MVC configuration is required to work, and so on. The `standaloneSetup` is also a very -convenient way to write ad-hoc tests to verify specific behavior or to debug an issue. - -As with most "`integration versus unit testing`" debates, there is no right or wrong -answer. However, using the `standaloneSetup` does imply the need for additional -`webAppContextSetup` tests in order to verify your Spring MVC configuration. -Alternatively, you can write all your tests with `webAppContextSetup`, in order to always -test against your actual Spring MVC configuration. - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-steps.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-steps.adoc deleted file mode 100644 index 2ec41f725a57..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-steps.adoc +++ /dev/null @@ -1,63 +0,0 @@ -[[spring-mvc-test-server-setup-steps]] -= Setup Features - -No matter which MockMvc builder you use, all `MockMvcBuilder` implementations provide -some common and very useful features. For example, you can declare an `Accept` header for -all requests and expect a status of 200 as well as a `Content-Type` header in all -responses, as follows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - // static import of MockMvcBuilders.standaloneSetup - - MockMvc mockMvc = standaloneSetup(new MusicController()) - .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON)) - .alwaysExpect(status().isOk()) - .alwaysExpect(content().contentType("application/json;charset=UTF-8")) - .build(); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ----- -====== - -In addition, third-party frameworks (and applications) can pre-package setup -instructions, such as those in a `MockMvcConfigurer`. The Spring Framework has one such -built-in implementation that helps to save and re-use the HTTP session across requests. -You can use it as follows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - // static import of SharedHttpSessionConfigurer.sharedHttpSession - - MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController()) - .apply(sharedHttpSession()) - .build(); - - // Use mockMvc to perform requests... ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ----- -====== - -See the javadoc for -{api-spring-framework}/test/web/servlet/setup/ConfigurableMockMvcBuilder.html[`ConfigurableMockMvcBuilder`] -for a list of all MockMvc builder features or use the IDE to explore the available options. - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-static-imports.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-static-imports.adoc deleted file mode 100644 index 21ccea19311e..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-static-imports.adoc +++ /dev/null @@ -1,18 +0,0 @@ -[[spring-mvc-test-server-static-imports]] -= Static Imports -:page-section-summary-toc: 1 - -When using MockMvc directly to perform requests, you'll need static imports for: - -- `MockMvcBuilders.{asterisk}` -- `MockMvcRequestBuilders.{asterisk}` -- `MockMvcResultMatchers.{asterisk}` -- `MockMvcResultHandlers.{asterisk}` - -An easy way to remember that is search for `MockMvc*`. If using Eclipse be sure to also -add the above as "`favorite static members`" in the Eclipse preferences. - -When using MockMvc through the xref:testing/webtestclient.adoc[WebTestClient] you do not need static imports. -The `WebTestClient` provides a fluent API without static imports. - - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server.adoc deleted file mode 100644 index 2368a5a96c41..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server.adoc +++ /dev/null @@ -1,24 +0,0 @@ -[[spring-mvc-test-server]] -= Overview -:page-section-summary-toc: 1 - -You can write plain unit tests for Spring MVC by instantiating a controller, injecting it -with dependencies, and calling its methods. However such tests do not verify request -mappings, data binding, message conversion, type conversion, validation, and nor -do they involve any of the supporting `@InitBinder`, `@ModelAttribute`, or -`@ExceptionHandler` methods. - -The Spring MVC Test framework, also known as `MockMvc`, aims to provide more complete -testing for Spring MVC controllers without a running server. It does that by invoking -the `DispatcherServlet` and passing -xref:testing/unit.adoc#mock-objects-servlet["`mock`" implementations of the Servlet API] from the -`spring-test` module which replicates the full Spring MVC request handling without -a running server. - -MockMvc is a server side test framework that lets you verify most of the functionality -of a Spring MVC application using lightweight and targeted tests. You can use it on -its own to perform requests and to verify responses, or you can also use it through -the xref:testing/webtestclient.adoc[WebTestClient] API with MockMvc plugged in as the server to handle requests -with. - - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc deleted file mode 100644 index 9b26c80f2370..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc +++ /dev/null @@ -1,46 +0,0 @@ -[[spring-mvc-test-vs-end-to-end-integration-tests]] -= MockMvc vs End-to-End Tests - -MockMVc is built on Servlet API mock implementations from the -`spring-test` module and does not rely on a running container. Therefore, there are -some differences when compared to full end-to-end integration tests with an actual -client and a live server running. - -The easiest way to think about this is by starting with a blank `MockHttpServletRequest`. -Whatever you add to it is what the request becomes. Things that may catch you by surprise -are that there is no context path by default; no `jsessionid` cookie; no forwarding, -error, or async dispatches; and, therefore, no actual JSP rendering. Instead, -"`forwarded`" and "`redirected`" URLs are saved in the `MockHttpServletResponse` and can -be asserted with expectations. - -This means that, if you use JSPs, you can verify the JSP page to which the request was -forwarded, but no HTML is rendered. In other words, the JSP is not invoked. Note, -however, that all other rendering technologies that do not rely on forwarding, such as -Thymeleaf and Freemarker, render HTML to the response body as expected. The same is true -for rendering JSON, XML, and other formats through `@ResponseBody` methods. - -Alternatively, you may consider the full end-to-end integration testing support from -Spring Boot with `@SpringBootTest`. See the -{docs-spring-boot}/html/spring-boot-features.html#boot-features-testing[Spring Boot Reference Guide]. - -There are pros and cons for each approach. The options provided in Spring MVC Test are -different stops on the scale from classic unit testing to full integration testing. To be -certain, none of the options in Spring MVC Test fall under the category of classic unit -testing, but they are a little closer to it. For example, you can isolate the web layer -by injecting mocked services into controllers, in which case you are testing the web -layer only through the `DispatcherServlet` but with actual Spring configuration, as you -might test the data access layer in isolation from the layers above it. Also, you can use -the stand-alone setup, focusing on one controller at a time and manually providing the -configuration required to make it work. - -Another important distinction when using Spring MVC Test is that, conceptually, such -tests are the server-side, so you can check what handler was used, if an exception was -handled with a HandlerExceptionResolver, what the content of the model is, what binding -errors there were, and other details. That means that it is easier to write expectations, -since the server is not an opaque box, as it is when testing it through an actual HTTP -client. This is generally an advantage of classic unit testing: It is easier to write, -reason about, and debug but does not replace the need for full integration tests. At the -same time, it is important not to lose sight of the fact that the response is the most -important thing to check. In short, there is room here for multiple styles and strategies -of testing even within the same project. - diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc deleted file mode 100644 index c5c3e7dc5a2c..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc +++ /dev/null @@ -1,38 +0,0 @@ -[[spring-mvc-test-vs-streaming-response]] -= Streaming Responses - -The best way to test streaming responses such as Server-Sent Events is through the -<> which can be used as a test client to connect to a `MockMvc` instance -to perform tests on Spring MVC controllers without a running server. For example: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build(); - - FluxExchangeResult exchangeResult = client.get() - .uri("/persons") - .exchange() - .expectStatus().isOk() - .expectHeader().contentType("text/event-stream") - .returnResult(Person.class); - - // Use StepVerifier from Project Reactor to test the streaming response - - StepVerifier.create(exchangeResult.getResponseBody()) - .expectNext(new Person("N0"), new Person("N1"), new Person("N2")) - .expectNextCount(4) - .consumeNextWith(person -> assertThat(person.getName()).endsWith("7")) - .thenCancel() - .verify(); ----- -====== - -`WebTestClient` can also connect to a live server and perform full end-to-end integration -tests. This is also supported in Spring Boot where you can -{docs-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server]. - - diff --git a/framework-docs/modules/ROOT/pages/testing/support-jdbc.adoc b/framework-docs/modules/ROOT/pages/testing/support-jdbc.adoc index cc09b76658d9..b2283e6723ec 100644 --- a/framework-docs/modules/ROOT/pages/testing/support-jdbc.adoc +++ b/framework-docs/modules/ROOT/pages/testing/support-jdbc.adoc @@ -31,5 +31,5 @@ provide convenience methods that delegate to the aforementioned methods in The `spring-jdbc` module provides support for configuring and launching an embedded database, which you can use in integration tests that interact with a database. For details, see xref:data-access/jdbc/embedded-database-support.adoc[Embedded Database Support] - and <>. + and xref:data-access/jdbc/embedded-database-support.adoc#jdbc-embedded-database-dao-testing[Testing Data Access +Logic with an Embedded Database]. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc index c9836157b55f..5348b383c4cf 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc @@ -19,6 +19,15 @@ following features. use an AOT-optimized `ApplicationContext` that participates transparently with the xref:testing/testcontext-framework/ctx-management/caching.adoc[context cache]. +All tests are enabled in AOT mode by default. However, you can selectively disable an +entire test class or individual test method in AOT mode by annotating it with +xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`]. +When using JUnit Jupiter, you may selectively enable or disable tests in a GraalVM native +image via Jupiter's `@EnabledInNativeImage` and `@DisabledInNativeImage` annotations. +Note that `@DisabledInAotMode` also disables the annotated test class or test method when +running within a GraalVM native image, analogous to JUnit Jupiter's +`@DisabledInNativeImage` annotation. + [TIP] ==== By default, if an error is encountered during build-time AOT processing, an exception @@ -36,20 +45,20 @@ xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. [NOTE] ==== -The `@ContextHierarchy` annotation is currently not supported in AOT mode. +The `@ContextHierarchy` annotation is not supported in AOT mode. ==== To provide test-specific runtime hints for use within a GraalVM native image, you have the following options. * Implement a custom - {api-spring-framework}/test/context/aot/TestRuntimeHintsRegistrar.html[`TestRuntimeHintsRegistrar`] + {spring-framework-api}/test/context/aot/TestRuntimeHintsRegistrar.html[`TestRuntimeHintsRegistrar`] and register it globally via `META-INF/spring/aot.factories`. -* Implement a custom {api-spring-framework}/aot/hint/RuntimeHintsRegistrar.html[`RuntimeHintsRegistrar`] +* Implement a custom {spring-framework-api}/aot/hint/RuntimeHintsRegistrar.html[`RuntimeHintsRegistrar`] and register it globally via `META-INF/spring/aot.factories` or locally on a test class - via {api-spring-framework}/context/annotation/ImportRuntimeHints.html[`@ImportRuntimeHints`]. -* Annotate a test class with {api-spring-framework}/aot/hint/annotation/Reflective.html[`@Reflective`] or - {api-spring-framework}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`]. + via {spring-framework-api}/context/annotation/ImportRuntimeHints.html[`@ImportRuntimeHints`]. +* Annotate a test class with {spring-framework-api}/aot/hint/annotation/Reflective.html[`@Reflective`] or + {spring-framework-api}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`]. * See xref:core/aot.adoc#aot.hints[Runtime Hints] for details on Spring's core runtime hints and annotation support. @@ -62,12 +71,12 @@ that are not specific to particular test classes, favor implementing ==== If you implement a custom `ContextLoader`, it must implement -{api-spring-framework}/test/context/aot/AotContextLoader.html[`AotContextLoader`] in +{spring-framework-api}/test/context/aot/AotContextLoader.html[`AotContextLoader`] in order to provide AOT build-time processing and AOT runtime execution support. Note, however, that all context loader implementations provided by the Spring Framework and Spring Boot already implement `AotContextLoader`. If you implement a custom `TestExecutionListener`, it must implement -{api-spring-framework}/test/context/aot/AotTestExecutionListener.html[`AotTestExecutionListener`] +{spring-framework-api}/test/context/aot/AotTestExecutionListener.html[`AotTestExecutionListener`] in order to participate in AOT processing. See the `SqlScriptsTestExecutionListener` in the `spring-test` module for an example. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc index 14cf022a8891..f0f40db96554 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc @@ -1,7 +1,7 @@ [[testcontext-application-events]] = Application Events -Since Spring Framework 5.3.3, the TestContext framework provides support for recording +The TestContext framework provides support for recording xref:core/beans/context-introduction.adoc#context-functionality-events[application events] published in the `ApplicationContext` so that assertions can be performed against those events within tests. All events published during the execution of a single test are made available via @@ -24,7 +24,7 @@ To use `ApplicationEvents` in your tests, do the following. to an `@Autowired` field in the test class. The following test class uses the `SpringExtension` for JUnit Jupiter and -https://assertj.github.io/doc/[AssertJ] to assert the types of application events +{assertj-docs}[AssertJ] to assert the types of application events published while invoking a method in a Spring-managed component: // Don't use "quotes" in the "subs" section because of the asterisks in /* ... */ @@ -32,7 +32,7 @@ published while invoking a method in a Spring-managed component: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @SpringJUnitConfig(/* ... */) @RecordApplicationEvents // <1> @@ -60,7 +60,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @SpringJUnitConfig(/* ... */) @RecordApplicationEvents // <1> @@ -88,6 +88,6 @@ Kotlin:: ====== See the -{api-spring-framework}/test/context/event/ApplicationEvents.html[`ApplicationEvents` +{spring-framework-api}/test/context/event/ApplicationEvents.html[`ApplicationEvents` javadoc] for further details regarding the `ApplicationEvents` API. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc new file mode 100644 index 000000000000..a709dd96e432 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc @@ -0,0 +1,89 @@ +[[testcontext-bean-overriding]] += Bean Overriding in Tests + +Bean overriding in tests refers to the ability to override specific beans in the +`ApplicationContext` for a test class, by annotating the test class or one or more +non-static fields in the test class. + +NOTE: This feature is intended as a less risky alternative to the practice of registering +a bean via `@Bean` with the `DefaultListableBeanFactory` +`setAllowBeanDefinitionOverriding` flag set to `true`. + +The Spring TestContext framework provides two sets of annotations for bean overriding. + +* xref:testing/annotations/integration-spring/annotation-testbean.adoc[`@TestBean`] +* xref:testing/annotations/integration-spring/annotation-mockitobean.adoc[`@MockitoBean` and `@MockitoSpyBean`] + +The former relies purely on Spring, while the latter set relies on the +https://site.mockito.org/[Mockito] third-party library. + +[[testcontext-bean-overriding-custom]] +== Custom Bean Override Support + +The three annotations mentioned above build upon the `@BeanOverride` meta-annotation and +associated infrastructure, which allows one to define custom bean overriding variants. + +To implement custom bean override support, the following is needed: + +* An annotation meta-annotated with `@BeanOverride` that defines the + `BeanOverrideProcessor` to use +* A custom `BeanOverrideProcessor` implementation +* One or more concrete `BeanOverrideHandler` implementations created by the processor + +The Spring TestContext framework includes implementations of the following APIs that +support bean overriding and are responsible for setting up the rest of the infrastructure. + +* a `BeanFactoryPostProcessor` +* a `ContextCustomizerFactory` +* a `TestExecutionListener` + +The `spring-test` module registers implementations of the latter two +(`BeanOverrideContextCustomizerFactory` and `BeanOverrideTestExecutionListener`) in its +{spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories` +properties file]. + +The bean overriding infrastructure searches for annotations on test classes as well as +annotations on non-static fields in test classes that are meta-annotated with +`@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is +responsible for creating an appropriate `BeanOverrideHandler`. + +The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to +alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as +defined by the corresponding `BeanOverrideStrategy`: + +[[testcontext-bean-overriding-strategy]] +`REPLACE`:: + Replaces the bean. Throws an exception if a corresponding bean does not exist. +`REPLACE_OR_CREATE`:: + Replaces the bean if it exists. Creates a new bean if a corresponding bean does not + exist. +`WRAP`:: + Retrieves the original bean and wraps it. + +[TIP] +==== +Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean +will result in an exception. + +When replacing a bean created by a `FactoryBean`, the `FactoryBean` itself will be +replaced with a singleton bean corresponding to bean override instance created by the +applicable `BeanOverrideHandler`. + +When wrapping a bean created by a `FactoryBean`, the object created by the `FactoryBean` +will be wrapped, not the `FactoryBean` itself. +==== + +[NOTE] +==== +In contrast to Spring's autowiring mechanism (for example, resolution of an `@Autowired` +field), the bean overriding infrastructure in the TestContext framework has limited +heuristics it can perform to locate a bean. Either the `BeanOverrideProcessor` can compute +the name of the bean to override, or it can be unambiguously selected given the type of +the annotated field and its qualifying annotations. + +Typically, the bean is selected "by type" by the `BeanOverrideFactoryPostProcessor`. +Alternatively, the user can directly provide the bean name in the custom annotation. + +`BeanOverrideProcessor` implementations may also internally compute a bean name based on +a convention or some other method. +==== diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc index 9aa446ed035e..880f7832e209 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management.adoc @@ -20,7 +20,7 @@ a field or setter method, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig class MyTest { @@ -35,7 +35,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig class MyTest { @@ -57,7 +57,7 @@ the web application context into your test, as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig // <1> class MyWebAppTest { @@ -73,7 +73,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig // <1> class MyWebAppTest { 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 1979482042cf..1698c6169291 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 @@ -29,7 +29,7 @@ You can register `ContextCustomizerFactory` implementations explicitly for a tes subclasses, and its nested classes by using the `@ContextCustomizerFactories` annotation. See xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[annotation support] and the javadoc for -{api-spring-framework}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories`] +{spring-framework-api}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories`] for details and examples. @@ -42,13 +42,15 @@ become cumbersome if a custom factory needs to be used across an entire test sui issue is addressed through support for automatic discovery of default `ContextCustomizerFactory` implementations through the `SpringFactoriesLoader` mechanism. -Specifically, the modules that make up the testing support in Spring Framework and Spring +For example, the modules that make up the testing support in Spring Framework and Spring Boot declare all core default `ContextCustomizerFactory` implementations under the `org.springframework.test.context.ContextCustomizerFactory` key in their -`META-INF/spring.factories` properties files. Third-party frameworks and developers can -contribute their own `ContextCustomizerFactory` implementations to the list of default -factories in the same manner through their own `META-INF/spring.factories` properties -files. +`META-INF/spring.factories` properties files. The `spring.factories` file for the +`spring-test` module can be viewed +{spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[here]. +Third-party frameworks and developers can contribute their own `ContextCustomizerFactory` +implementations to the list of default factories in the same manner through their own +`spring.factories` files. [[testcontext-context-customizers-merging]] diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc index ad418ef4cdfc..1eb927a41854 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc @@ -1,48 +1,75 @@ [[testcontext-ctx-management-dynamic-property-sources]] = Context Configuration with Dynamic Property Sources -As of Spring Framework 5.2.5, the TestContext framework provides support for _dynamic_ -properties via the `@DynamicPropertySource` annotation. This annotation can be used in -integration tests that need to add properties with dynamic values to the set of -`PropertySources` in the `Environment` for the `ApplicationContext` loaded for the -integration test. +The Spring TestContext Framework provides support for _dynamic_ properties via the +`DynamicPropertyRegistry`, the `@DynamicPropertySource` annotation, and the +`DynamicPropertyRegistrar` API. [NOTE] ==== -The `@DynamicPropertySource` annotation and its supporting infrastructure were -originally designed to allow properties from -https://www.testcontainers.org/[Testcontainers] based tests to be exposed easily to -Spring integration tests. However, this feature may also be used with any form of -external resource whose lifecycle is maintained outside the test's `ApplicationContext`. +The dynamic property source infrastructure was originally designed to allow properties +from {testcontainers-site}[Testcontainers] based tests to be exposed easily to Spring +integration tests. However, these features may be used with any form of external resource +whose lifecycle is managed outside the test's `ApplicationContext` or with beans whose +lifecycle is managed by the test's `ApplicationContext`. ==== -In contrast to the xref:testing/testcontext-framework/ctx-management/property-sources.adoc[`@TestPropertySource`] -annotation that is applied at the class level, `@DynamicPropertySource` must be applied -to a `static` method that accepts a single `DynamicPropertyRegistry` argument which is -used to add _name-value_ pairs to the `Environment`. Values are dynamic and provided via -a `Supplier` which is only invoked when the property is resolved. Typically, method -references are used to supply values, as can be seen in the following example which uses -the Testcontainers project to manage a Redis container outside of the Spring -`ApplicationContext`. The IP address and port of the managed Redis container are made -available to components within the test's `ApplicationContext` via the `redis.host` and -`redis.port` properties. These properties can be accessed via Spring's `Environment` -abstraction or injected directly into Spring-managed components – for example, via -`@Value("${redis.host}")` and `@Value("${redis.port}")`, respectively. + +[[testcontext-ctx-management-dynamic-property-sources-precedence]] +== Precedence + +Dynamic properties have higher precedence than those loaded from `@TestPropertySource`, +the operating system's environment, Java system properties, or property sources added by +the application declaratively by using `@PropertySource` or programmatically. Thus, +dynamic properties can be used to selectively override properties loaded via +`@TestPropertySource`, system property sources, and application property sources. + + +[[testcontext-ctx-management-dynamic-property-sources-dynamic-property-registry]] +== `DynamicPropertyRegistry` + +A `DynamicPropertyRegistry` is used to add _name-value_ pairs to the `Environment`. +Values are dynamic and provided via a `Supplier` which is only invoked when the property +is resolved. Typically, method references are used to supply values. The following +sections provide examples of how to use the `DynamicPropertyRegistry`. + + +[[testcontext-ctx-management-dynamic-property-sources-dynamic-property-source]] +== `@DynamicPropertySource` + +In contrast to the +xref:testing/testcontext-framework/ctx-management/property-sources.adoc[`@TestPropertySource`] +annotation that is applied at the class level, `@DynamicPropertySource` can be applied to +`static` methods in integration test classes in order to add properties with dynamic +values to the set of `PropertySources` in the `Environment` for the `ApplicationContext` +loaded for the integration test. + +Methods in integration test classes that are annotated with `@DynamicPropertySource` must +be `static` and must accept a single `DynamicPropertyRegistry` argument. See the +class-level javadoc for `DynamicPropertyRegistry` for further details. [TIP] ==== If you use `@DynamicPropertySource` in a base class and discover that tests in subclasses fail because the dynamic properties change between subclasses, you may need to annotate -your base class with xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[`@DirtiesContext`] to -ensure that each subclass gets its own `ApplicationContext` with the correct dynamic +your base class with +xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[`@DirtiesContext`] +to ensure that each subclass gets its own `ApplicationContext` with the correct dynamic properties. ==== +The following example uses the Testcontainers project to manage a Redis container outside +of the Spring `ApplicationContext`. The IP address and port of the managed Redis +container are made available to components within the test's `ApplicationContext` via the +`redis.host` and `redis.port` properties. These properties can be accessed via Spring's +`Environment` abstraction or injected directly into Spring-managed components – for +example, via `@Value("${redis.host}")` and `@Value("${redis.port}")`, respectively. + [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(/* ... */) @Testcontainers @@ -65,7 +92,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(/* ... */) @Testcontainers @@ -92,12 +119,71 @@ Kotlin:: ---- ====== -[[precedence]] -== Precedence -Dynamic properties have higher precedence than those loaded from `@TestPropertySource`, -the operating system's environment, Java system properties, or property sources added by -the application declaratively by using `@PropertySource` or programmatically. Thus, -dynamic properties can be used to selectively override properties loaded via -`@TestPropertySource`, system property sources, and application property sources. +[[testcontext-ctx-management-dynamic-property-sources-dynamic-property-registrar]] +== `DynamicPropertyRegistrar` + +As an alternative to implementing `@DynamicPropertySource` methods in integration test +classes, you can register implementations of the `DynamicPropertyRegistrar` API as beans +within the test's `ApplicationContext`. Doing so allows you to support additional use +cases that are not possible with a `@DynamicPropertySource` method. For example, since a +`DynamicPropertyRegistrar` is itself a bean in the `ApplicationContext`, it can interact +with other beans in the context and register dynamic properties that are sourced from +those beans. + +Any bean in a test's `ApplicationContext` that implements the `DynamicPropertyRegistrar` +interface will be automatically detected and eagerly initialized before the singleton +pre-instantiation phase, and the `accept()` methods of such beans will be invoked with a +`DynamicPropertyRegistry` that performs the actual dynamic property registration on +behalf of the registrar. +WARNING: Any interaction with other beans results in eager initialization of those other +beans and their dependencies. + +The following example demonstrates how to implement a `DynamicPropertyRegistrar` as a +lambda expression that registers a dynamic property for the `ApiServer` bean. The +`api.url` property can be accessed via Spring's `Environment` abstraction or injected +directly into other Spring-managed components – for example, via `@Value("${api.url}")`, +and the value of the `api.url` property will be dynamically retrieved from the +`ApiServer` bean. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + class TestConfig { + + @Bean + ApiServer apiServer() { + return new ApiServer(); + } + + @Bean + DynamicPropertyRegistrar apiPropertiesRegistrar(ApiServer apiServer) { + return registry -> registry.add("api.url", apiServer::getUrl); + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Configuration + class TestConfig { + + @Bean + fun apiServer(): ApiServer { + return ApiServer() + } + + @Bean + fun apiPropertiesRegistrar(apiServer: ApiServer): DynamicPropertyRegistrar { + return registry -> registry.add("api.url", apiServer::getUrl) + } + } +---- +====== diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/env-profiles.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/env-profiles.adoc index bb868bf5fae4..ea0c505e6643 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/env-profiles.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/env-profiles.adoc @@ -63,7 +63,7 @@ Consider two examples with XML configuration and `@Configuration` classes: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "classpath:/app-config.xml" @@ -83,7 +83,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from "classpath:/app-config.xml" @@ -128,7 +128,7 @@ integration test with `@Configuration` classes instead of XML: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("dev") @@ -147,7 +147,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("dev") @@ -169,7 +169,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("production") @@ -185,7 +185,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("production") @@ -204,7 +204,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("default") @@ -222,7 +222,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("default") @@ -243,7 +243,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class TransferServiceConfig { @@ -269,7 +269,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class TransferServiceConfig { @@ -299,7 +299,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig({ TransferServiceConfig.class, @@ -321,7 +321,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig( TransferServiceConfig::class, @@ -366,14 +366,14 @@ automatically inherit the `@ActiveProfiles` configuration from the base class. I following example, the declaration of `@ActiveProfiles` (as well as other annotations) has been moved to an abstract superclass, `AbstractIntegrationTest`: -NOTE: As of Spring Framework 5.3, test configuration may also be inherited from enclosing -classes. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] for details. +NOTE: Test configuration may also be inherited from enclosing classes. See +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] for details. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig({ TransferServiceConfig.class, @@ -387,7 +387,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig( TransferServiceConfig::class, @@ -404,7 +404,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // "dev" profile inherited from superclass class TransferServiceTest extends AbstractIntegrationTest { @@ -421,7 +421,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // "dev" profile inherited from superclass class TransferServiceTest : AbstractIntegrationTest() { @@ -444,7 +444,7 @@ disable the inheritance of active profiles, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // "dev" profile overridden with "production" @ActiveProfiles(profiles = "production", inheritProfiles = false) @@ -455,7 +455,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // "dev" profile overridden with "production" @ActiveProfiles("production", inheritProfiles = false) @@ -478,7 +478,7 @@ programmatically instead of declaratively -- for example, based on: To resolve active bean definition profiles programmatically, you can implement a custom `ActiveProfilesResolver` and register it by using the `resolver` attribute of `@ActiveProfiles`. For further information, see the corresponding -{api-spring-framework}/test/context/ActiveProfilesResolver.html[javadoc]. +{spring-framework-api}/test/context/ActiveProfilesResolver.html[javadoc]. The following example demonstrates how to implement and register a custom `OperatingSystemActiveProfilesResolver`: @@ -486,7 +486,7 @@ The following example demonstrates how to implement and register a custom ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // "dev" profile overridden programmatically via a custom resolver @ActiveProfiles( @@ -499,7 +499,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // "dev" profile overridden programmatically via a custom resolver @ActiveProfiles( @@ -515,7 +515,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver { @@ -530,7 +530,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver { diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/groovy.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/groovy.adoc index 9049bfc9ee22..43ead704e62b 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/groovy.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/groovy.adoc @@ -2,7 +2,7 @@ = Context Configuration with Groovy Scripts To load an `ApplicationContext` for your tests by using Groovy scripts that use the -xref:core/beans/basics.adoc#groovy-bean-definition-dsl[Groovy Bean Definition DSL], you can annotate +xref:core/beans/basics.adoc#beans-factory-groovy[Groovy Bean Definition DSL], you can annotate your test class with `@ContextConfiguration` and configure the `locations` or `value` attribute with an array that contains the resource locations of Groovy scripts. Resource lookup semantics for Groovy scripts are the same as those described for @@ -18,7 +18,7 @@ The following example shows how to specify Groovy configuration files: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "/AppConfig.groovy" and @@ -32,7 +32,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from "/AppConfig.groovy" and @@ -58,7 +58,7 @@ the default: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from @@ -72,7 +72,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from @@ -101,7 +101,7 @@ The following listing shows how to combine both in an integration test: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from @@ -114,7 +114,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from 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 c92ebb9064b2..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** -- @@ -39,7 +50,7 @@ lowest context in the hierarchy). The following listing shows this configuration ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @WebAppConfiguration @@ -58,7 +69,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) @WebAppConfiguration @@ -95,7 +106,7 @@ configuration scenario: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @WebAppConfiguration @@ -111,7 +122,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) @WebAppConfiguration @@ -146,7 +157,7 @@ The following listing shows this configuration scenario: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @ContextHierarchy({ @@ -163,7 +174,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) @ContextHierarchy( @@ -192,7 +203,7 @@ shows this configuration scenario: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @ContextHierarchy({ @@ -212,7 +223,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) @ContextHierarchy( @@ -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 -{api-spring-framework}/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/ctx-management/inheritance.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/inheritance.adoc index 0ca98c0965a7..b84d7f93c256 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/inheritance.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/inheritance.adoc @@ -17,8 +17,8 @@ is set to `false`, the resource locations or component classes and the context initializers, respectively, for the test class shadow and effectively replace the configuration defined by superclasses. -NOTE: As of Spring Framework 5.3, test configuration may also be inherited from enclosing -classes. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] for details. +NOTE: Test configuration may also be inherited from enclosing classes. See +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] for details. In the next example, which uses XML resource locations, the `ApplicationContext` for `ExtendedTest` is loaded from `base-config.xml` and `extended-config.xml`, in that order. @@ -30,7 +30,7 @@ another and use both its own configuration file and the superclass's configurati ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "/base-config.xml" @@ -52,7 +52,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from "/base-config.xml" @@ -84,7 +84,7 @@ another and use both its own configuration class and the superclass's configurat ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // ApplicationContext will be loaded from BaseConfig @SpringJUnitConfig(BaseConfig.class) // <1> @@ -103,7 +103,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ApplicationContext will be loaded from BaseConfig @SpringJUnitConfig(BaseConfig::class) // <1> @@ -133,7 +133,7 @@ extend another and use both its own initializer and the superclass's initializer ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // ApplicationContext will be initialized by BaseInitializer @SpringJUnitConfig(initializers = BaseInitializer.class) // <1> @@ -153,7 +153,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ApplicationContext will be initialized by BaseInitializer @SpringJUnitConfig(initializers = [BaseInitializer::class]) // <1> diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/initializers.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/initializers.adoc index ce05e1a382e6..6eccba2f0a41 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/initializers.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/initializers.adoc @@ -17,7 +17,7 @@ order in which the initializers are invoked depends on whether they implement Sp ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from TestConfig @@ -33,7 +33,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from TestConfig @@ -59,7 +59,7 @@ files or configuration classes. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be initialized by EntireAppInitializer @@ -73,7 +73,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be initialized by EntireAppInitializer diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/javaconfig.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/javaconfig.adoc index cfd0cedfe235..ba96142ef9ee 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/javaconfig.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/javaconfig.adoc @@ -10,7 +10,7 @@ that contains references to component classes. The following example shows how t ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from AppConfig and TestConfig @@ -23,7 +23,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from AppConfig and TestConfig @@ -51,8 +51,8 @@ The term "`component class`" can refer to any of the following: of a single constructor without the use of Spring annotations. See the javadoc of -{api-spring-framework}/context/annotation/Configuration.html[`@Configuration`] and -{api-spring-framework}/context/annotation/Bean.html[`@Bean`] for further information +{spring-framework-api}/context/annotation/Configuration.html[`@Configuration`] and +{spring-framework-api}/context/annotation/Bean.html[`@Bean`] for further information regarding the configuration and semantics of component classes, paying special attention to the discussion of `@Bean` Lite Mode. ==== @@ -62,7 +62,7 @@ TestContext framework tries to detect the presence of default configuration clas Specifically, `AnnotationConfigContextLoader` and `AnnotationConfigWebContextLoader` detect all `static` nested classes of the test class that meet the requirements for configuration class implementations, as specified in the -{api-spring-framework}/context/annotation/Configuration.html[`@Configuration`] javadoc. +{spring-framework-api}/context/annotation/Configuration.html[`@Configuration`] javadoc. Note that the name of the configuration class is arbitrary. In addition, a test class can contain more than one `static` nested configuration class if desired. In the following example, the `OrderServiceTest` class declares a `static` nested configuration class @@ -73,7 +73,7 @@ class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig <1> // ApplicationContext will be loaded from the static nested Config class @@ -105,7 +105,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig <1> // ApplicationContext will be loaded from the nested Config class diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc index 34057860b856..c0990b7396dc 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc @@ -50,7 +50,7 @@ The following example uses a test properties file: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource("/test.properties") // <1> @@ -62,7 +62,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource("/test.properties") // <1> @@ -106,7 +106,7 @@ The following example sets two inlined properties: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource(properties = {"timezone = GMT", "port = 4242"}) // <1> @@ -118,7 +118,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource(properties = ["timezone = GMT", "port = 4242"]) // <1> @@ -137,7 +137,7 @@ a text block: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource(properties = """ @@ -152,7 +152,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource(properties = [""" @@ -168,7 +168,8 @@ Kotlin:: [NOTE] ==== -As of Spring Framework 5.2, `@TestPropertySource` can be used as _repeatable annotation_. +`@TestPropertySource` can be used as _repeatable annotation_. + That means that you can have multiple declarations of `@TestPropertySource` on a single test class, with the `locations` and `properties` from later `@TestPropertySource` annotations overriding those from previous `@TestPropertySource` annotations. @@ -184,7 +185,6 @@ meta-present `@TestPropertySource` annotations. In other words, `locations` and meta-annotation. ==== - [[default-properties-file-detection]] == Default Properties File Detection @@ -195,7 +195,7 @@ if the annotated test class is `com.example.MyTest`, the corresponding default p file is `classpath:com/example/MyTest.properties`. If the default cannot be detected, an `IllegalStateException` is thrown. -[[precedence]] +[[testcontext-ctx-management-property-sources-precedence]] == Precedence Test properties have higher precedence than those defined in the operating system's @@ -218,7 +218,7 @@ to specify properties both in a file and inline: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource( @@ -232,7 +232,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestPropertySource("/test.properties", @@ -262,8 +262,8 @@ If the `inheritLocations` or `inheritProperties` attribute in `@TestPropertySour set to `false`, the locations or inlined properties, respectively, for the test class shadow and effectively replace the configuration defined by superclasses. -NOTE: As of Spring Framework 5.3, test configuration may also be inherited from enclosing -classes. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] for details. +NOTE: Test configuration may also be inherited from enclosing classes. See +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] for details. In the next example, the `ApplicationContext` for `BaseTest` is loaded by using only the `base.properties` file as a test property source. In contrast, the `ApplicationContext` @@ -275,7 +275,7 @@ properties in both a subclass and its superclass by using `properties` files: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @TestPropertySource("base.properties") @ContextConfiguration @@ -292,7 +292,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @TestPropertySource("base.properties") @ContextConfiguration @@ -317,7 +317,7 @@ to define properties in both a subclass and its superclass by using inline prope ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @TestPropertySource(properties = "key1 = value1") @ContextConfiguration @@ -334,7 +334,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @TestPropertySource(properties = ["key1 = value1"]) @ContextConfiguration diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/web-mocks.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/web-mocks.adoc index 267baf8e0e0b..578a3f88733d 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/web-mocks.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/web-mocks.adoc @@ -20,9 +20,9 @@ managed per test method by the `ServletTestExecutionListener`. [tabs] ====== -Injecting mocks:: +Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig class WacTests { @@ -51,7 +51,7 @@ Injecting mocks:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig class WacTests { diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/web.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/web.adoc index cfc6778bbb95..34b6d671f869 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/web.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/web.adoc @@ -29,11 +29,12 @@ The remaining examples in this section show some of the various configuration op loading a `WebApplicationContext`. The following example shows the TestContext framework's support for convention over configuration: +.Conventions [tabs] ====== -Conventions:: +Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @@ -50,7 +51,7 @@ Conventions:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) @@ -76,11 +77,12 @@ as the `WacTests` class or static nested `@Configuration` classes). The following example shows how to explicitly declare a resource base path with `@WebAppConfiguration` and an XML resource location with `@ContextConfiguration`: +.Default resource semantics [tabs] ====== -Default resource semantics:: +Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @@ -96,7 +98,7 @@ Default resource semantics:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) @@ -118,11 +120,12 @@ whereas `@ContextConfiguration` resource locations are classpath based. The following example shows that we can override the default resource semantics for both annotations by specifying a Spring resource prefix: +.Explicit resource semantics [tabs] ====== -Explicit resource semantics:: +Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @@ -138,7 +141,7 @@ Explicit resource semantics:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/xml.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/xml.adoc index 78e998e43ca9..71e0b27f3342 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/xml.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/xml.adoc @@ -14,7 +14,7 @@ path that represents a resource URL (i.e., a path prefixed with `classpath:`, `f ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "/app-config.xml" and @@ -28,7 +28,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from "/app-config.xml" and @@ -52,7 +52,7 @@ demonstrated in the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @ContextConfiguration({"/app-config.xml", "/test-config.xml"}) <1> @@ -64,7 +64,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) @ContextConfiguration("/app-config.xml", "/test-config.xml") // <1> @@ -88,7 +88,7 @@ example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from @@ -102,7 +102,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from 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 dc51cfa9d0dc..967f774288f5 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 @@ -29,7 +29,7 @@ integration test methods. scripts and is mainly intended for internal use within the framework. However, if you require full control over how SQL scripts are parsed and run, `ScriptUtils` may suit your needs better than some of the other alternatives described later. See the -{api-spring-framework}/jdbc/datasource/init/ScriptUtils.html[javadoc] for individual +{spring-framework-api}/jdbc/datasource/init/ScriptUtils.html[javadoc] for individual methods in `ScriptUtils` for further details. `ResourceDatabasePopulator` provides an object-based API for programmatically populating, @@ -38,7 +38,7 @@ resources. `ResourceDatabasePopulator` provides options for configuring the char encoding, statement separator, comment delimiters, and error handling flags used when parsing and running the scripts. Each of the configuration options has a reasonable default value. See the -{api-spring-framework}/jdbc/datasource/init/ResourceDatabasePopulator.html[javadoc] for +{spring-framework-api}/jdbc/datasource/init/ResourceDatabasePopulator.html[javadoc] for details on default values. To run the scripts configured in a `ResourceDatabasePopulator`, you can invoke either the `populate(Connection)` method to run the populator against a `java.sql.Connection` or the `execute(DataSource)` method @@ -50,7 +50,7 @@ specifies SQL scripts for a test schema and test data, sets the statement separa ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Test void databaseTest() { @@ -66,7 +66,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test fun databaseTest() { @@ -95,13 +95,22 @@ In addition to the aforementioned mechanisms for running SQL scripts programmati you can declaratively configure SQL scripts in the Spring TestContext Framework. Specifically, you can declare the `@Sql` annotation on a test class or test method to configure individual SQL statements or the resource paths to SQL scripts that should be -run against a given database before or after an integration test method. Support for -`@Sql` is provided by the `SqlScriptsTestExecutionListener`, which is enabled by default. - -NOTE: Method-level `@Sql` declarations override class-level declarations by default. As -of Spring Framework 5.2, however, this behavior may be configured per test class or per -test method via `@SqlMergeMode`. See -xref:testing/testcontext-framework/executing-sql.adoc#testcontext-executing-sql-declaratively-script-merging[Merging and Overriding Configuration with `@SqlMergeMode`] for further details. +run against a given database before or after an integration test class or test method. +Support for `@Sql` is provided by the `SqlScriptsTestExecutionListener`, which is enabled +by default. + +[NOTE] +==== +Method-level `@Sql` declarations override class-level declarations by default, but this +behavior may be configured per test class or per test method via `@SqlMergeMode`. See +xref:testing/testcontext-framework/executing-sql.adoc#testcontext-executing-sql-declaratively-script-merging[Merging and Overriding Configuration with `@SqlMergeMode`] +for further details. + +However, this does not apply to class-level declarations configured for the +`BEFORE_TEST_CLASS` or `AFTER_TEST_CLASS` execution phases. Such declarations cannot be +overridden, and the corresponding scripts and statements will be executed once per class +in addition to any method-level scripts and statements. +==== [[testcontext-executing-sql-declaratively-script-resources]] === Path Resource Semantics @@ -113,6 +122,9 @@ classpath resource (for example, `"/org/example/schema.sql"`). A path that refer URL (for example, a path prefixed with `classpath:`, `file:`, `http:`) is loaded by using the specified resource protocol. +As of Spring Framework 6.2, paths may contain property placeholders (`${...}`) that will +be replaced by properties stored in the `Environment` of the test's `ApplicationContext`. + The following example shows how to use `@Sql` at the class level and at the method level within a JUnit Jupiter based integration test class: @@ -120,7 +132,7 @@ within a JUnit Jupiter based integration test class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig @Sql("/test-schema.sql") @@ -141,7 +153,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig @Sql("/test-schema.sql") @@ -174,23 +186,31 @@ script, depending on where `@Sql` is declared. If a default cannot be detected, defined in the class `com.example.MyTest`, the corresponding default script is `classpath:com/example/MyTest.testMethod.sql`. +[[testcontext-executing-sql-declaratively-logging]] +=== Logging SQL Scripts and Statements + +If you want to see which SQL scripts are being executed, set the +`org.springframework.test.context.jdbc` logging category to `DEBUG`. + +If you want to see which SQL statements are being executed, set the +`org.springframework.jdbc.datasource.init` logging category to `DEBUG`. + [[testcontext-executing-sql-declaratively-multiple-annotations]] === Declaring Multiple `@Sql` Sets If you need to configure multiple sets of SQL scripts for a given test class or test method but with different syntax configuration, different error handling rules, or -different execution phases per set, you can declare multiple instances of `@Sql`. With -Java 8, you can use `@Sql` as a repeatable annotation. Otherwise, you can use the -`@SqlGroup` annotation as an explicit container for declaring multiple instances of -`@Sql`. +different execution phases per set, you can declare multiple instances of `@Sql`. You can +either use `@Sql` as a repeatable annotation, or you can use the `@SqlGroup` annotation +as an explicit container for declaring multiple instances of `@Sql`. -The following example shows how to use `@Sql` as a repeatable annotation with Java 8: +The following example shows how to use `@Sql` as a repeatable annotation: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Test @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")) @@ -202,9 +222,14 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - // Repeatable annotations with non-SOURCE retention are not yet supported by Kotlin + @Test + @Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")) + @Sql("/test-user-data.sql") + fun userTest() { + // run code that uses the test schema and test data + } ---- ====== @@ -212,15 +237,14 @@ In the scenario presented in the preceding example, the `test-schema.sql` script different syntax for single-line comments. The following example is identical to the preceding example, except that the `@Sql` -declarations are grouped together within `@SqlGroup`. With Java 8 and above, the use of -`@SqlGroup` is optional, but you may need to use `@SqlGroup` for compatibility with -other JVM languages such as Kotlin. +declarations are grouped together within `@SqlGroup`. The use of `@SqlGroup` is optional, +but you may need to use `@SqlGroup` for compatibility with other JVM languages. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Test @SqlGroup({ @@ -234,12 +258,13 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test @SqlGroup( Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")), - Sql("/test-user-data.sql")) + Sql("/test-user-data.sql") + ) fun userTest() { // Run code that uses the test schema and test data } @@ -249,16 +274,16 @@ Kotlin:: [[testcontext-executing-sql-declaratively-script-execution-phases]] === Script Execution Phases -By default, SQL scripts are run before the corresponding test method. However, if -you need to run a particular set of scripts after the test method (for example, to clean -up database state), you can use the `executionPhase` attribute in `@Sql`, as the -following example shows: +By default, SQL scripts are run before the corresponding test method. However, if you +need to run a particular set of scripts after the test method (for example, to clean up +database state), you can set the `executionPhase` attribute in `@Sql` to +`AFTER_TEST_METHOD`, as the following example shows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Test @Sql( @@ -278,15 +303,14 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test - @SqlGroup( - Sql("create-test-data.sql", - config = SqlConfig(transactionMode = ISOLATED)), - Sql("delete-test-data.sql", - config = SqlConfig(transactionMode = ISOLATED), - executionPhase = AFTER_TEST_METHOD)) + @Sql("create-test-data.sql", + config = SqlConfig(transactionMode = ISOLATED)) + @Sql("delete-test-data.sql", + config = SqlConfig(transactionMode = ISOLATED), + executionPhase = AFTER_TEST_METHOD) fun userTest() { // run code that needs the test data to be committed // to the database outside of the test's transaction @@ -294,9 +318,60 @@ Kotlin:: ---- ====== -Note that `ISOLATED` and `AFTER_TEST_METHOD` are statically imported from +NOTE: `ISOLATED` and `AFTER_TEST_METHOD` are statically imported from `Sql.TransactionMode` and `Sql.ExecutionPhase`, respectively. +As of Spring Framework 6.1, it is possible to run a particular set of scripts before or +after the test class by setting the `executionPhase` attribute in a class-level `@Sql` +declaration to `BEFORE_TEST_CLASS` or `AFTER_TEST_CLASS`, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig + @Sql(scripts = "/test-schema.sql", executionPhase = BEFORE_TEST_CLASS) + class DatabaseTests { + + @Test + void emptySchemaTest() { + // run code that uses the test schema without any test data + } + + @Test + @Sql("/test-user-data.sql") + void userTest() { + // run code that uses the test schema and test data + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig + @Sql("/test-schema.sql", executionPhase = BEFORE_TEST_CLASS) + class DatabaseTests { + + @Test + fun emptySchemaTest() { + // run code that uses the test schema without any test data + } + + @Test + @Sql("/test-user-data.sql") + fun userTest() { + // run code that uses the test schema and test data + } + } +---- +====== + +NOTE: `BEFORE_TEST_CLASS` is statically imported from `Sql.ExecutionPhase`. + [[testcontext-executing-sql-declaratively-script-configuration]] === Script Configuration with `@SqlConfig` @@ -320,11 +395,11 @@ local `@SqlConfig` attributes do not supply an explicit value other than `""`, ` The configuration options provided by `@Sql` and `@SqlConfig` are equivalent to those supported by `ScriptUtils` and `ResourceDatabasePopulator` but are a superset of those provided by the `` XML namespace element. See the javadoc of -individual attributes in {api-spring-framework}/test/context/jdbc/Sql.html[`@Sql`] and -{api-spring-framework}/test/context/jdbc/SqlConfig.html[`@SqlConfig`] for details. +individual attributes in {spring-framework-api}/test/context/jdbc/Sql.html[`@Sql`] and +{spring-framework-api}/test/context/jdbc/SqlConfig.html[`@SqlConfig`] for details. [[testcontext-executing-sql-declaratively-tx]] -*Transaction management for `@Sql`* +==== Transaction management for `@Sql` By default, the `SqlScriptsTestExecutionListener` infers the desired transaction semantics for scripts configured by using `@Sql`. Specifically, SQL scripts are run @@ -343,8 +418,8 @@ behavior by setting the `transactionMode` attribute of `@SqlConfig` (for example scripts should be run in an isolated transaction). Although a thorough discussion of all supported options for transaction management with `@Sql` is beyond the scope of this reference manual, the javadoc for -{api-spring-framework}/test/context/jdbc/SqlConfig.html[`@SqlConfig`] and -{api-spring-framework}/test/context/jdbc/SqlScriptsTestExecutionListener.html[`SqlScriptsTestExecutionListener`] +{spring-framework-api}/test/context/jdbc/SqlConfig.html[`@SqlConfig`] and +{spring-framework-api}/test/context/jdbc/SqlScriptsTestExecutionListener.html[`SqlScriptsTestExecutionListener`] provide detailed information, and the following example shows a typical testing scenario that uses JUnit Jupiter and transactional tests with `@Sql`: @@ -352,7 +427,7 @@ that uses JUnit Jupiter and transactional tests with `@Sql`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestDatabaseConfig.class) @Transactional @@ -386,7 +461,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestDatabaseConfig::class) @Transactional @@ -423,7 +498,7 @@ details). [[testcontext-executing-sql-declaratively-script-merging]] === Merging and Overriding Configuration with `@SqlMergeMode` -As of Spring Framework 5.2, it is possible to merge method-level `@Sql` declarations with +It is possible to merge method-level `@Sql` declarations with class-level declarations. For example, this allows you to provide the configuration for a database schema or some common test data once per test class and then provide additional, use case specific test data per test method. To enable `@Sql` merging, annotate either diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/fixture-di.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/fixture-di.adoc index 54ce51bffef2..c0d352e4393f 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/fixture-di.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/fixture-di.adoc @@ -58,7 +58,7 @@ uses `@Autowired` for field injection: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // specifies the Spring configuration to load for this test fixture @@ -79,7 +79,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // specifies the Spring configuration to load for this test fixture @@ -106,7 +106,7 @@ follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) // specifies the Spring configuration to load for this test fixture @@ -131,7 +131,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension::class) // specifies the Spring configuration to load for this test fixture @@ -192,7 +192,7 @@ method in the superclass as well): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // ... @@ -207,7 +207,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ... diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/key-abstractions.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/key-abstractions.adoc index 04e5e9ce4aa8..6910ce06fb9e 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/key-abstractions.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/key-abstractions.adoc @@ -10,7 +10,7 @@ in turn, manages a `TestContext` that holds the context of the current test. The and delegates to `TestExecutionListener` implementations, which instrument the actual test execution by providing dependency injection, managing transactions, and so on. A `SmartContextLoader` is responsible for loading an `ApplicationContext` for a given test -class. See the {api-spring-framework}/test/context/package-summary.html[javadoc] and the +class. See the {spring-framework-api}/test/context/package-summary.html[javadoc] and the Spring test suite for further information and examples of various implementations. [[testcontext]] 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 1a3c642f6261..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 @@ -1,10 +1,9 @@ [[testcontext-parallel-test-execution]] = Parallel Test Execution -Spring Framework 5.0 introduced basic support for executing tests in parallel within a -single JVM when using the Spring TestContext Framework. In general, this means that most -test classes or test methods can be run in parallel without any changes to test code -or configuration. +The Spring TestContext Framework provides basic support for executing tests in parallel +within a single JVM. In general, this means that most test classes or test methods can be +run in parallel without any changes to test code or configuration. TIP: For details on how to set up parallel test execution, see the documentation for your testing framework, build tool, or IDE. @@ -17,10 +16,11 @@ for when not to run tests in parallel. 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. @@ -40,9 +40,8 @@ for details. WARNING: Parallel test execution in the Spring TestContext Framework is only possible if the underlying `TestContext` implementation provides a copy constructor, as explained in -the javadoc for {api-spring-framework}/test/context/TestContext.html[`TestContext`]. The +the javadoc for {spring-framework-api}/test/context/TestContext.html[`TestContext`]. The `DefaultTestContext` used in Spring provides such a constructor. However, if you use a third-party library that provides a custom `TestContext` implementation, you need to verify that it is suitable for parallel test execution. - 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 51731166829f..1ee5856ecfd0 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 @@ -24,7 +24,7 @@ run with the custom Spring `Runner`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RunWith(SpringRunner.class) @TestExecutionListeners({}) @@ -39,7 +39,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RunWith(SpringRunner::class) @TestExecutionListeners @@ -83,7 +83,7 @@ to declare these rules in an integration test: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Optionally specify a non-Spring Runner via @RunWith(...) @ContextConfiguration @@ -104,7 +104,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Optionally specify a non-Spring Runner via @RunWith(...) @ContextConfiguration @@ -193,7 +193,7 @@ The following code listing shows how to configure a test class to use the ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Instructs JUnit Jupiter to extend the test with Spring support. @ExtendWith(SpringExtension.class) @@ -210,7 +210,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Instructs JUnit Jupiter to extend the test with Spring support. @ExtendWith(SpringExtension::class) @@ -237,7 +237,7 @@ used in the previous example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Instructs Spring to register the SpringExtension with JUnit // Jupiter and load an ApplicationContext from TestConfig.class @@ -253,7 +253,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Instructs Spring to register the SpringExtension with JUnit // Jupiter and load an ApplicationContext from TestConfig.class @@ -275,7 +275,7 @@ Similarly, the following example uses `@SpringJUnitWebConfig` to create a ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Instructs Spring to register the SpringExtension with JUnit // Jupiter and load a WebApplicationContext from TestWebConfig.class @@ -291,7 +291,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Instructs Spring to register the SpringExtension with JUnit // Jupiter and load a WebApplicationContext from TestWebConfig::class @@ -376,7 +376,7 @@ In the following example, Spring injects the `OrderService` bean from the ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) class OrderServiceIntegrationTests { @@ -394,7 +394,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) class OrderServiceIntegrationTests @Autowired constructor(private val orderService: OrderService){ @@ -414,7 +414,7 @@ xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-anno ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) class OrderServiceIntegrationTests { @@ -431,7 +431,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) class OrderServiceIntegrationTests(val orderService:OrderService) { @@ -455,7 +455,7 @@ loaded from `TestConfig.class` into the `deleteOrder()` test method: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) class OrderServiceIntegrationTests { @@ -469,7 +469,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) class OrderServiceIntegrationTests { @@ -493,7 +493,7 @@ into the `placeOrderRepeatedly()` test method simultaneously. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) class OrderServiceIntegrationTests { @@ -510,7 +510,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) class OrderServiceIntegrationTests { @@ -531,12 +531,8 @@ to the `RepetitionInfo`. [[testcontext-junit-jupiter-nested-test-configuration]] === `@Nested` test class configuration -The _Spring TestContext Framework_ has supported the use of test-related annotations on -`@Nested` test classes in JUnit Jupiter since Spring Framework 5.0; however, until Spring -Framework 5.3 class-level test configuration annotations were not _inherited_ from -enclosing classes like they are from superclasses. - -Spring Framework 5.3 introduced first-class support for inheriting test class +The _Spring TestContext Framework_ supports the use of test-related annotations on `@Nested` +test classes in JUnit Jupiter, including first-class support for inheriting test class configuration from enclosing classes, and such configuration will be inherited by default. To change from the default `INHERIT` mode to `OVERRIDE` mode, you may annotate an individual `@Nested` test class with @@ -546,6 +542,14 @@ any of its subclasses and nested classes. Thus, you may annotate a top-level tes with `@NestedTestConfiguration`, and that will apply to all of its nested test classes recursively. +[TIP] +==== +If you are developing a component that integrates with the Spring TestContext Framework +and needs to support annotation inheritance within enclosing class hierarchies, you must +use the annotation search utilities provided in `TestContextAnnotationUtils` in order to +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 @@ -565,7 +569,7 @@ which annotations can be inherited in `@Nested` test classes. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) class GreetingServiceTests { @@ -594,7 +598,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) class GreetingServiceTests { diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc index 2c2c749b88cb..1d6c2d64979e 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc @@ -7,21 +7,26 @@ by default, exactly in the following order: * `ServletTestExecutionListener`: Configures Servlet API mocks for a `WebApplicationContext`. * `DirtiesContextBeforeModesTestExecutionListener`: Handles the `@DirtiesContext` - annotation for "`before`" modes. + annotation for "before" modes. * `ApplicationEventsTestExecutionListener`: Provides support for xref:testing/testcontext-framework/application-events.adoc[`ApplicationEvents`]. +* `BeanOverrideTestExecutionListener`: Provides support for + xref:testing/testcontext-framework/bean-overriding.adoc[]. * `DependencyInjectionTestExecutionListener`: Provides dependency injection for the test instance. * `MicrometerObservationRegistryTestExecutionListener`: Provides support for Micrometer's `ObservationRegistry`. * `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for - "`after`" modes. + "after" modes. +* `CommonCachesTestExecutionListener`: Clears resource caches in the test's + `ApplicationContext` if necessary. * `TransactionalTestExecutionListener`: Provides transactional test execution with default rollback semantics. * `SqlScriptsTestExecutionListener`: Runs SQL scripts configured by using the `@Sql` annotation. * `EventPublishingTestExecutionListener`: Publishes test execution events to the test's `ApplicationContext` (see xref:testing/testcontext-framework/test-execution-events.adoc[Test Execution Events]). +* `MockitoResetTestExecutionListener`: Resets mocks as configured by `@MockitoBean` or `@MockitoSpyBean`. [[testcontext-tel-config-registering-tels]] == Registering `TestExecutionListener` Implementations @@ -29,7 +34,7 @@ by default, exactly in the following order: You can register `TestExecutionListener` implementations explicitly for a test class, its subclasses, and its nested classes by using the `@TestExecutionListeners` annotation. See xref:testing/annotations.adoc[annotation support] and the javadoc for -{api-spring-framework}/test/context/TestExecutionListeners.html[`@TestExecutionListeners`] +{spring-framework-api}/test/context/TestExecutionListeners.html[`@TestExecutionListeners`] for details and examples. .Switching to default `TestExecutionListener` implementations @@ -43,7 +48,7 @@ following. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Switch to default listeners @TestExecutionListeners( @@ -57,7 +62,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Switch to default listeners @TestExecutionListeners( @@ -80,12 +85,12 @@ become cumbersome if a custom listener needs to be used across an entire test su issue is addressed through support for automatic discovery of default `TestExecutionListener` implementations through the `SpringFactoriesLoader` mechanism. -Specifically, the `spring-test` module declares all core default `TestExecutionListener` +For example, the `spring-test` module declares all core default `TestExecutionListener` implementations under the `org.springframework.test.context.TestExecutionListener` key in -its `META-INF/spring.factories` properties file. Third-party frameworks and developers -can contribute their own `TestExecutionListener` implementations to the list of default -listeners in the same manner through their own `META-INF/spring.factories` properties -file. +its {spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories` +properties file]. Third-party frameworks and developers can contribute their own +`TestExecutionListener` implementations to the list of default listeners in the same +manner through their own `spring.factories` files. [[testcontext-tel-config-ordering]] == Ordering `TestExecutionListener` Implementations @@ -114,7 +119,7 @@ listeners. The following listing demonstrates this style of configuration: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestExecutionListeners({ @@ -133,7 +138,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestExecutionListeners( @@ -157,15 +162,16 @@ change from release to release -- for example, `SqlScriptsTestExecutionListener` introduced in Spring Framework 4.1, and `DirtiesContextBeforeModesTestExecutionListener` was introduced in Spring Framework 4.2. Furthermore, third-party frameworks like Spring Boot and Spring Security register their own default `TestExecutionListener` -implementations by using the aforementioned xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-automatic-discovery[automatic discovery mechanism] -. +implementations by using the aforementioned +xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-automatic-discovery[automatic discovery mechanism]. To avoid having to be aware of and re-declare all default listeners, you can set the `mergeMode` attribute of `@TestExecutionListeners` to `MergeMode.MERGE_WITH_DEFAULTS`. `MERGE_WITH_DEFAULTS` indicates that locally declared listeners should be merged with the default listeners. The merging algorithm ensures that duplicates are removed from the list and that the resulting set of merged listeners is sorted according to the semantics -of `AnnotationAwareOrderComparator`, as described in xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-ordering[Ordering `TestExecutionListener` Implementations]. +of `AnnotationAwareOrderComparator`, as described in +xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-ordering[Ordering `TestExecutionListener` Implementations]. If a listener implements `Ordered` or is annotated with `@Order`, it can influence the position in which it is merged with the defaults. Otherwise, locally declared listeners are appended to the list of default listeners when merged. @@ -181,7 +187,7 @@ be replaced with the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestExecutionListeners( @@ -195,7 +201,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestExecutionListeners( @@ -207,4 +213,3 @@ Kotlin:: } ---- ====== - diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/test-execution-events.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/test-execution-events.adoc index b83c12736731..922f9e2fb4c9 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/test-execution-events.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/test-execution-events.adoc @@ -1,9 +1,9 @@ [[testcontext-test-execution-events]] = Test Execution Events -The `EventPublishingTestExecutionListener` introduced in Spring Framework 5.2 offers an -alternative approach to implementing a custom `TestExecutionListener`. Components in the -test's `ApplicationContext` can listen to the following events published by the +The `EventPublishingTestExecutionListener` offers an alternative approach to implementing +a custom `TestExecutionListener`. Components in the test's `ApplicationContext` can +listen to the following events published by the `EventPublishingTestExecutionListener`, each of which corresponds to a method in the `TestExecutionListener` API. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc index d144ddc2f771..875e24a21f8a 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc @@ -106,7 +106,7 @@ a Hibernate-based `UserRepository`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) @Transactional @@ -150,7 +150,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) @Transactional @@ -216,14 +216,14 @@ Support for `TestTransaction` is automatically available whenever the `TransactionalTestExecutionListener` is enabled. The following example demonstrates some of the features of `TestTransaction`. See the -javadoc for {api-spring-framework}/test/context/transaction/TestTransaction.html[`TestTransaction`] +javadoc for {spring-framework-api}/test/context/transaction/TestTransaction.html[`TestTransaction`] for further details. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration(classes = TestConfig.class) public class ProgrammaticTransactionManagementTests extends @@ -255,7 +255,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration(classes = [TestConfig::class]) class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() { @@ -316,7 +316,7 @@ example. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @BeforeTransaction void verifyInitialDatabaseState(@Autowired DataSource dataSource) { @@ -326,7 +326,7 @@ void verifyInitialDatabaseState(@Autowired DataSource dataSource) { Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @BeforeTransaction fun verifyInitialDatabaseState(@Autowired dataSource: DataSource) { @@ -355,7 +355,7 @@ of `PlatformTransactionManager` within the test's `ApplicationContext`, you can qualifier by using `@Transactional("myTxMgr")` or `@Transactional(transactionManager = "myTxMgr")`, or `TransactionManagementConfigurer` can be implemented by an `@Configuration` class. Consult the -{api-spring-framework}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager-org.springframework.test.context.TestContext-java.lang.String-[javadoc +{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager-org.springframework.test.context.TestContext-java.lang.String-[javadoc for `TestContextTransactionUtils.retrieveTransactionManager()`] for details on the algorithm used to look up a transaction manager in the test's `ApplicationContext`. @@ -375,7 +375,7 @@ following example shows the relevant annotations: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig @Transactional(transactionManager = "txMgr") @@ -414,7 +414,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig @Transactional(transactionManager = "txMgr") @@ -469,7 +469,7 @@ session: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // ... @@ -497,7 +497,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ... @@ -530,7 +530,7 @@ The following example shows matching methods for JPA: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // ... @@ -558,7 +558,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ... @@ -612,7 +612,7 @@ example. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // ... @@ -640,7 +640,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ... @@ -668,7 +668,7 @@ Kotlin:: ====== See -https://github.com/spring-projects/spring-framework/blob/main/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java[JpaEntityListenerTests] +{spring-framework-code}/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java[JpaEntityListenerTests] in the Spring Framework test suite for working examples using all JPA lifecycle callbacks. ===== 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 20e7926a7f59..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 @@ -47,11 +47,12 @@ the provided `MockHttpServletRequest`. When the `loginUser()` method is invoked set parameters). We can then perform assertions against the results based on the known inputs for the username and password. The following listing shows how to do so: +.Request-scoped bean test [tabs] ====== -Request-scoped bean test:: +Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig class RequestScopedBeanTests { @@ -72,7 +73,7 @@ Request-scoped bean test:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig class RequestScopedBeanTests { @@ -95,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"] @@ -124,11 +123,12 @@ the user service has access to the session-scoped `userPreferences` for the curr `MockHttpSession`, and we can perform assertions against the results based on the configured theme. The following example shows how to do so: +.Session-scoped bean test [tabs] ====== -Session-scoped bean test:: +Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig class SessionScopedBeanTests { @@ -148,7 +148,7 @@ Session-scoped bean test:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig class SessionScopedBeanTests { diff --git a/framework-docs/modules/ROOT/pages/testing/unit.adoc b/framework-docs/modules/ROOT/pages/testing/unit.adoc index f137331a5138..ddc70fd18f8e 100644 --- a/framework-docs/modules/ROOT/pages/testing/unit.adoc +++ b/framework-docs/modules/ROOT/pages/testing/unit.adoc @@ -26,7 +26,6 @@ are described in this chapter. Spring includes a number of packages dedicated to mocking: * xref:testing/unit.adoc#mock-objects-env[Environment] -* xref:testing/unit.adoc#mock-objects-jndi[JNDI] * xref:testing/unit.adoc#mock-objects-servlet[Servlet API] * xref:testing/unit.adoc#mock-objects-web-reactive[Spring Web Reactive] @@ -42,34 +41,19 @@ and xref:core/beans/environment.adoc#beans-property-source-abstraction[`Property out-of-container tests for code that depends on environment-specific properties. -[[mock-objects-jndi]] -=== JNDI - -The `org.springframework.mock.jndi` package contains a partial implementation of the JNDI -SPI, which you can use to set up a simple JNDI environment for test suites or stand-alone -applications. If, for example, JDBC `DataSource` instances get bound to the same JNDI -names in test code as they do in a Jakarta EE container, you can reuse both application code -and configuration in testing scenarios without modification. - -WARNING: The mock JNDI support in the `org.springframework.mock.jndi` package is -officially deprecated as of Spring Framework 5.2 in favor of complete solutions from third -parties such as https://github.com/h-thurow/Simple-JNDI[Simple-JNDI]. - - [[mock-objects-servlet]] === Servlet API 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. -The Spring MVC Test framework builds on the mock Servlet API objects to provide an -integration testing framework for Spring MVC. See xref:testing/spring-mvc-test-framework.adoc[MockMvc]. +MockMvc builds on the mock Servlet API objects to provide an integration testing +framework for Spring MVC. See xref:testing/mockmvc.adoc[MockMvc]. [[mock-objects-web-reactive]] @@ -112,16 +96,16 @@ categories: The `org.springframework.test.util` package contains several general purpose utilities for use in unit and integration testing. -{api-spring-framework}/test/util/AopTestUtils.html[`AopTestUtils`] is a collection of +{spring-framework-api}/test/util/AopTestUtils.html[`AopTestUtils`] is a collection of AOP-related utility methods. You can use these methods to obtain a reference to the underlying target object hidden behind one or more Spring proxies. For example, if you have configured a bean as a dynamic mock by using a library such as EasyMock or Mockito, and the mock is wrapped in a Spring proxy, you may need direct access to the underlying mock to configure expectations on it and perform verifications. For Spring's core AOP -utilities, see {api-spring-framework}/aop/support/AopUtils.html[`AopUtils`] and -{api-spring-framework}/aop/framework/AopProxyUtils.html[`AopProxyUtils`]. +utilities, see {spring-framework-api}/aop/support/AopUtils.html[`AopUtils`] and +{spring-framework-api}/aop/framework/AopProxyUtils.html[`AopProxyUtils`]. -{api-spring-framework}/test/util/ReflectionTestUtils.html[`ReflectionTestUtils`] is a +{spring-framework-api}/test/util/ReflectionTestUtils.html[`ReflectionTestUtils`] is a collection of reflection-based utility methods. You can use these methods in testing scenarios where you need to change the value of a constant, set a non-`public` field, invoke a non-`public` setter method, or invoke a non-`public` configuration or lifecycle @@ -135,7 +119,7 @@ callback method when testing application code for use cases such as the followin * Use of annotations such as `@PostConstruct` and `@PreDestroy` for lifecycle callback methods. -{api-spring-framework}/test/util/TestSocketUtils.html[`TestSocketUtils`] is a simple +{spring-framework-api}/test/util/TestSocketUtils.html[`TestSocketUtils`] is a simple utility for finding available TCP ports on `localhost` for use in integration testing scenarios. @@ -155,7 +139,7 @@ server for the port it is currently using. === Spring MVC Testing Utilities The `org.springframework.test.web` package contains -{api-spring-framework}/test/web/ModelAndViewAssert.html[`ModelAndViewAssert`], which you +{spring-framework-api}/test/web/ModelAndViewAssert.html[`ModelAndViewAssert`], which you can use in combination with JUnit, TestNG, or any other testing framework for unit tests that deal with Spring MVC `ModelAndView` objects. @@ -165,4 +149,4 @@ combined with `MockHttpServletRequest`, `MockHttpSession`, and so on from Spring xref:testing/unit.adoc#mock-objects-servlet[Servlet API mocks]. For thorough integration testing of your Spring MVC and REST `Controller` classes in conjunction with your `WebApplicationContext` configuration for Spring MVC, use the -xref:testing/spring-mvc-test-framework.adoc[Spring MVC Test Framework] instead. +xref:testing/mockmvc.adoc[MockMvc] instead. diff --git a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc index 16a44856931c..2642be67edf7 100644 --- a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc +++ b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc @@ -7,9 +7,6 @@ but exposes a testing facade for verifying responses. `WebTestClient` can be use perform end-to-end HTTP tests. It can also be used to test Spring MVC and Spring WebFlux applications without a running server via mock server request and response objects. -TIP: Kotlin users: See xref:languages/kotlin/spring-projects-in.adoc#kotlin-webtestclient-issue[this section] -related to use of the `WebTestClient`. - @@ -36,7 +33,7 @@ to handle requests: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebTestClient client = WebTestClient.bindToController(new TestController()).build(); @@ -44,23 +41,23 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = WebTestClient.bindToController(TestController()).build() ---- ====== For Spring MVC, use the following which delegates to the -{api-spring-framework}/test/web/servlet/setup/StandaloneMockMvcBuilder.html[StandaloneMockMvcBuilder] +{spring-framework-api}/test/web/servlet/setup/StandaloneMockMvcBuilder.html[StandaloneMockMvcBuilder] to load infrastructure equivalent to the xref:web/webmvc/mvc-config.adoc[WebMvc Java config], registers the given controller(s), and creates an instance of -xref:testing/spring-mvc-test-framework.adoc[MockMvc] to handle requests: +xref:testing/mockmvc.adoc[MockMvc] to handle requests: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebTestClient client = MockMvcWebTestClient.bindToController(new TestController()).build(); @@ -68,7 +65,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = MockMvcWebTestClient.bindToController(TestController()).build() ---- @@ -84,7 +81,7 @@ infrastructure and controller declarations and use it to handle requests via moc and response objects, without a running server. For WebFlux, use the following where the Spring `ApplicationContext` is passed to -{api-spring-framework}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder] +{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder] to create the xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[WebHandler chain] to handle requests: @@ -92,7 +89,7 @@ requests: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(WebConfig.class) // <1> class MyTests { @@ -111,7 +108,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(WebConfig::class) // <1> class MyTests { @@ -130,15 +127,15 @@ Kotlin:: ====== For Spring MVC, use the following where the Spring `ApplicationContext` is passed to -{api-spring-framework}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-[MockMvcBuilders.webAppContextSetup] -to create a xref:testing/spring-mvc-test-framework.adoc[MockMvc] instance to handle +{spring-framework-api}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup(org.springframework.web.context.WebApplicationContext)[MockMvcBuilders.webAppContextSetup] +to create a xref:testing/mockmvc.adoc[MockMvc] instance to handle requests: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @WebAppConfiguration("classpath:META-INF/web-resources") // <1> @@ -165,7 +162,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ExtendWith(SpringExtension.class) @WebAppConfiguration("classpath:META-INF/web-resources") // <1> @@ -196,7 +193,7 @@ Kotlin:: [[webtestclient-fn-config]] === Bind to Router Function -This setup allows you to test <> via +This setup allows you to test xref:web/webflux-functional.adoc[functional endpoints] via mock request and response objects, without a running server. For WebFlux, use the following which delegates to `RouterFunctions.toWebHandler` to @@ -206,7 +203,7 @@ create a server setup to handle requests: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RouterFunction route = ... client = WebTestClient.bindToRouterFunction(route).build(); @@ -214,7 +211,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val route: RouterFunction<*> = ... val client = WebTestClient.bindToRouterFunction(route).build() @@ -235,14 +232,14 @@ This setup connects to a running server to perform full, end-to-end HTTP tests: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build(); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build() ---- @@ -263,7 +260,7 @@ follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client = WebTestClient.bindToController(new TestController()) .configureClient() @@ -273,7 +270,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client = WebTestClient.bindToController(TestController()) .configureClient() @@ -302,7 +299,7 @@ To assert the response status and headers, use the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons/1") .accept(MediaType.APPLICATION_JSON) @@ -313,7 +310,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons/1") .accept(MediaType.APPLICATION_JSON) @@ -332,7 +329,7 @@ JUnit Jupiter. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons/1") .accept(MediaType.APPLICATION_JSON) @@ -342,6 +339,19 @@ Java:: spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) ); ---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectAll( + { spec -> spec.expectStatus().isOk() }, + { spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) } + ) +---- ====== You can then choose to decode the response body through one of the following: @@ -356,7 +366,7 @@ And perform assertions on the resulting higher level Object(s): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons") .exchange() @@ -366,7 +376,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.test.web.reactive.server.expectBodyList @@ -384,7 +394,7 @@ perform any other assertions: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.test.web.reactive.server.expectBody @@ -393,20 +403,20 @@ Java:: .expectStatus().isOk() .expectBody(Person.class) .consumeWith(result -> { - // custom assertions (e.g. AssertJ)... + // custom assertions (for example, AssertJ)... }); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons/1") .exchange() .expectStatus().isOk() .expectBody() .consumeWith { - // custom assertions (e.g. AssertJ)... + // custom assertions (for example, AssertJ)... } ---- ====== @@ -417,7 +427,7 @@ Or you can exit the workflow and obtain an `EntityExchangeResult`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- EntityExchangeResult result = client.get().uri("/persons/1") .exchange() @@ -428,7 +438,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.test.web.reactive.server.expectBody @@ -442,7 +452,7 @@ Kotlin:: TIP: When you need to decode to a target type with generics, look for the overloaded methods that accept -{api-spring-framework}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`] +{spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`] instead of `Class`. @@ -456,7 +466,7 @@ If the response is not expected to have content, you can assert that as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client.post().uri("/persons") .body(personMono, Person.class) @@ -467,7 +477,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.post().uri("/persons") .bodyValue(person) @@ -484,7 +494,7 @@ any assertions: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons/123") .exchange() @@ -494,7 +504,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons/123") .exchange() @@ -517,7 +527,7 @@ To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAsse ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons/1") .exchange() @@ -528,7 +538,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons/1") .exchange() @@ -544,7 +554,7 @@ To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons") .exchange() @@ -556,7 +566,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons") .exchange() @@ -580,7 +590,7 @@ obtain a `FluxExchangeResult`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- FluxExchangeResult result = client.get().uri("/events") .accept(TEXT_EVENT_STREAM) @@ -592,7 +602,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.test.web.reactive.server.returnResult @@ -610,7 +620,7 @@ Now you're ready to consume the response stream with `StepVerifier` from `reacto ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Flux eventFlux = result.getResponseBody(); @@ -624,7 +634,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val eventFlux = result.getResponseBody() @@ -652,7 +662,7 @@ obtaining an `ExchangeResult` after asserting the body: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // For a response with a body EntityExchangeResult result = client.get().uri("/persons/1") @@ -669,19 +679,19 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // For a response with a body val result = client.get().uri("/persons/1") .exchange() .expectStatus().isOk() - .expectBody(Person.class) - .returnResult(); + .expectBody() + .returnResult() // For a response without a body val result = client.get().uri("/path") .exchange() - .expectBody().isEmpty(); + .expectBody().isEmpty() ---- ====== @@ -691,7 +701,7 @@ Then switch to MockMvc server response assertions: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- MockMvcWebTestClient.resultActionsFor(result) .andExpect(model().attribute("integer", 3)) @@ -700,11 +710,10 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- MockMvcWebTestClient.resultActionsFor(result) .andExpect(model().attribute("integer", 3)) .andExpect(model().attribute("string", "a string value")); ---- ====== - diff --git a/framework-docs/modules/ROOT/pages/web-reactive.adoc b/framework-docs/modules/ROOT/pages/web-reactive.adoc index b7ef832ffa33..378d65ef5911 100644 --- a/framework-docs/modules/ROOT/pages/web-reactive.adoc +++ b/framework-docs/modules/ROOT/pages/web-reactive.adoc @@ -2,10 +2,11 @@ = Web on Reactive Stack This part of the documentation covers support for reactive-stack web applications built -on a https://www.reactive-streams.org/[Reactive Streams] API to run on non-blocking +on a {reactive-streams-site}/[Reactive Streams] API to run on non-blocking servers, such as Netty, Undertow, and Servlet containers. Individual chapters cover the xref:web/webflux.adoc#webflux[Spring WebFlux] framework, -the reactive xref:web/webflux-webclient.adoc[`WebClient`], support for xref:web-reactive.adoc#webflux-test[testing], -and xref:web-reactive.adoc#webflux-reactive-libraries[reactive libraries]. For Servlet-stack web applications, -see xref:web.adoc[Web on Servlet Stack]. +the reactive xref:web/webflux-webclient.adoc[`WebClient`], +support for xref:web/webflux-test.adoc[testing], +and xref:web/webflux-reactive-libraries.adoc[reactive libraries]. For Servlet-stack web +applications, see xref:web.adoc[Web on Servlet Stack]. diff --git a/framework-docs/modules/ROOT/pages/web.adoc b/framework-docs/modules/ROOT/pages/web.adoc index 76ebcfc90689..e8a18f927c88 100644 --- a/framework-docs/modules/ROOT/pages/web.adoc +++ b/framework-docs/modules/ROOT/pages/web.adoc @@ -5,9 +5,5 @@ This part of the documentation covers support for Servlet-stack web applications built on the Servlet API and deployed to Servlet containers. Individual chapters include xref:web/webmvc.adoc#mvc[Spring MVC], xref:web/webmvc-view.adoc[View Technologies], xref:web/webmvc-cors.adoc[CORS Support], and xref:web/websocket.adoc[WebSocket Support]. -For reactive-stack web applications, see xref:testing/unit.adoc#mock-objects-web-reactive[Web on Reactive Stack]. - - - - +For reactive-stack web applications, see xref:web-reactive.adoc[Web on Reactive Stack]. diff --git a/framework-docs/modules/ROOT/pages/web/integration.adoc b/framework-docs/modules/ROOT/pages/web/integration.adoc index 6d362d916ae7..ed280eb57b1d 100644 --- a/framework-docs/modules/ROOT/pages/web/integration.adoc +++ b/framework-docs/modules/ROOT/pages/web/integration.adoc @@ -35,7 +35,7 @@ context"). This section details how you can configure a Spring container (a `WebApplicationContext`) that contains all of the 'business beans' in your application. Moving on to specifics, all you need to do is declare a -{api-spring-framework}/web/context/ContextLoaderListener.html[`ContextLoaderListener`] +{spring-framework-api}/web/context/ContextLoaderListener.html[`ContextLoaderListener`] in the standard Jakarta EE servlet `web.xml` file of your web application and add a `contextConfigLocation` `` section (in the same file) that defines which set of Spring XML configuration files to load. @@ -62,7 +62,7 @@ Further consider the following `` configuration: If you do not specify the `contextConfigLocation` context parameter, the `ContextLoaderListener` looks for a file called `/WEB-INF/applicationContext.xml` to load. Once the context files are loaded, Spring creates a -{api-spring-framework}/web/context/WebApplicationContext.html[`WebApplicationContext`] +{spring-framework-api}/web/context/WebApplicationContext.html[`WebApplicationContext`] object based on the bean definitions and stores it in the `ServletContext` of the web application. @@ -78,7 +78,7 @@ The following example shows how to get the `WebApplicationContext`: ---- The -{api-spring-framework}/web/context/support/WebApplicationContextUtils.html[`WebApplicationContextUtils`] +{spring-framework-api}/web/context/support/WebApplicationContextUtils.html[`WebApplicationContextUtils`] class is for convenience, so you need not remember the name of the `ServletContext` attribute. Its `getWebApplicationContext()` method returns `null` if an object does not exist under the `WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE` @@ -103,7 +103,7 @@ on its specific integration strategies. JavaServer Faces (JSF) is the JCP's standard component-based, event-driven web user interface framework. It is an official part of the Jakarta EE umbrella but also -individually usable, e.g. through embedding Mojarra or MyFaces within Tomcat. +individually usable, for example, through embedding Mojarra or MyFaces within Tomcat. Please note that recent versions of JSF became closely tied to CDI infrastructure in application servers, with some new JSF functionality only working in such an @@ -142,7 +142,7 @@ Configuration-wise, you can define `SpringBeanFacesELResolver` in your JSF A custom `ELResolver` works well when mapping your properties to beans in `faces-config.xml`, but, at times, you may need to explicitly grab a bean. -The {api-spring-framework}/web/jsf/FacesContextUtils.html[`FacesContextUtils`] +The {spring-framework-api}/web/jsf/FacesContextUtils.html[`FacesContextUtils`] class makes this easy. It is similar to `WebApplicationContextUtils`, except that it takes a `FacesContext` parameter rather than a `ServletContext` parameter. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc b/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc index 845976b8e02c..5553837d8a06 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc @@ -1,5 +1,6 @@ [[webflux-cors]] = CORS + [.small]#xref:web/webmvc-cors.adoc[See equivalent in the Servlet stack]# Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section @@ -47,7 +48,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r and, consequently, browsers reject them. Each `HandlerMapping` can be -{api-spring-framework}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured] +{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured] individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications use the WebFlux Java configuration to declare such mappings, which results in a single, global map passed to all `HandlerMapping` implementations. @@ -60,7 +61,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement The rules for combining global and local configuration are generally additive -- for example, all global and all local origins. For those attributes where only a single value can be accepted, such as `allowCredentials` and `maxAge`, the local overrides the global value. See -{api-spring-framework}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`] +{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`] for more details. [TIP] @@ -108,7 +109,7 @@ a finite set of values instead to provide a higher level of security. == `@CrossOrigin` [.small]#xref:web/webmvc-cors.adoc#mvc-cors-controller[See equivalent in the Servlet stack]# -The {api-spring-framework}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] +The {spring-framework-api}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] annotation enables cross-origin requests on annotated controller methods, as the following example shows: @@ -117,7 +118,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/account") @@ -138,7 +139,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/account") @@ -181,7 +182,7 @@ The following example specifies a certain domain and sets `maxAge` to an hour: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin(origins = "https://domain2.com", maxAge = 3600) @RestController @@ -202,7 +203,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin("https://domain2.com", maxAge = 3600) @RestController @@ -231,7 +232,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin(maxAge = 3600) // <1> @RestController @@ -255,7 +256,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin(maxAge = 3600) // <1> @RestController @@ -311,7 +312,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -334,7 +335,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -363,8 +364,8 @@ Kotlin:: [.small]#xref:web/webmvc-cors.adoc#mvc-cors-filter[See equivalent in the Servlet stack]# You can apply CORS support through the built-in -{api-spring-framework}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a -good fit with <>. +{spring-framework-api}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a +good fit with xref:web/webflux-functional.adoc[functional endpoints]. NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for @@ -377,7 +378,7 @@ To configure the filter, you can declare a `CorsWebFilter` bean and pass a ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Bean CorsWebFilter corsFilter() { @@ -401,7 +402,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Bean fun corsFilter(): CorsWebFilter { diff --git a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc index 3e3cb23c9f86..ae6368f8d309 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc @@ -1,5 +1,6 @@ [[webflux-fn]] = Functional Endpoints + [.small]#xref:web/webmvc-functional.adoc[See equivalent in the Servlet stack]# Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions @@ -34,7 +35,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.reactive.function.server.RequestPredicates.*; @@ -71,7 +72,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val repository: PersonRepository = ... val handler = PersonHandler(repository) @@ -122,7 +123,7 @@ Most applications can run through the WebFlux Java configuration, see xref:web/w `ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly access to the HTTP request and response. -Both request and response provide https://www.reactive-streams.org[Reactive Streams] back pressure +Both request and response provide {reactive-streams-site}[Reactive Streams] back pressure against the body streams. The request body is represented with a Reactor `Flux` or `Mono`. The response body is represented with any Reactive Streams `Publisher`, including `Flux` and `Mono`. @@ -142,14 +143,14 @@ The following example extracts the request body to a `Mono`: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Mono string = request.bodyToMono(String.class); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val string = request.awaitBody() ---- @@ -163,14 +164,14 @@ where `Person` objects are decoded from some serialized form, such as JSON or XM ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Flux people = request.bodyToFlux(Person.class); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val people = request.bodyToFlow() ---- @@ -185,7 +186,7 @@ also be written as follows: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Mono string = request.body(BodyExtractors.toMono(String.class)); Flux people = request.body(BodyExtractors.toFlux(Person.class)); @@ -193,7 +194,7 @@ Flux people = request.body(BodyExtractors.toFlux(Person.class)); Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle() val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow() @@ -206,14 +207,14 @@ The following example shows how to access form data: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Mono> map = request.formData(); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val map = request.awaitFormData() ---- @@ -225,14 +226,14 @@ The following example shows how to access multipart data as a map: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Mono> map = request.multipartData(); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val map = request.awaitMultipartData() ---- @@ -244,7 +245,7 @@ The following example shows how to access multipart data, one at a time, in stre ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Flux allPartEvents = request.bodyToFlux(PartEvent.class); allPartsEvents.windowUntil(PartEvent::isLast) @@ -272,7 +273,7 @@ allPartsEvents.windowUntil(PartEvent::isLast) Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parts = request.bodyToFlux() allPartsEvents.windowUntil(PartEvent::isLast) @@ -313,7 +314,7 @@ content: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Mono person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class); @@ -321,7 +322,7 @@ ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person. Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val person: Person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person) @@ -334,7 +335,7 @@ The following example shows how to build a 201 (CREATED) response with a `Locati ====== Java:: + -[source,java,role="primary"] +[source,java] ---- URI location = ... ServerResponse.created(location).build(); @@ -342,7 +343,7 @@ ServerResponse.created(location).build(); Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val location: URI = ... ServerResponse.created(location).build() @@ -350,20 +351,20 @@ ServerResponse.created(location).build() ====== Depending on the codec used, it is possible to pass hint parameters to customize how the -body is serialized or deserialized. For example, to specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON view]: +body is serialized or deserialized. For example, to specify a {baeldung-blog}/jackson-json-view-annotation[Jackson JSON view]: [tabs] ====== Java:: + -[source,java,role="primary"] +[source,java] ---- ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...) ---- @@ -380,7 +381,7 @@ We can write a handler function as a lambda, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HandlerFunction helloWorld = request -> ServerResponse.ok().bodyValue("Hello World"); @@ -388,7 +389,7 @@ HandlerFunction helloWorld = Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val helloWorld = HandlerFunction { ServerResponse.ok().bodyValue("Hello World") } ---- @@ -406,7 +407,7 @@ For example, the following class exposes a reactive `Person` repository: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.reactive.function.server.ServerResponse.ok; @@ -450,7 +451,7 @@ found. If it is not found, we use `switchIfEmpty(Mono)` to return a 404 Not F Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class PersonHandler(private val repository: PersonRepository) { @@ -495,7 +496,7 @@ xref:web/webmvc/mvc-config/validation.adoc[Validator] implementation for a `Pers ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class PersonHandler { @@ -523,7 +524,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class PersonHandler(private val repository: PersonRepository) { @@ -593,7 +594,7 @@ header: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RouterFunction route = RouterFunctions.route() .GET("/hello-world", accept(MediaType.TEXT_PLAIN), @@ -602,7 +603,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val route = coRouter { GET("/hello-world", accept(TEXT_PLAIN)) { @@ -652,7 +653,7 @@ The following example shows the composition of four routes: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.reactive.function.server.RequestPredicates.*; @@ -679,7 +680,7 @@ RouterFunction route = route() Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.http.MediaType.APPLICATION_JSON @@ -719,7 +720,7 @@ improved in the following way by using nested routes: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RouterFunction route = route() .path("/person", builder -> builder // <1> @@ -732,7 +733,7 @@ RouterFunction route = route() Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val route = coRouter { // <1> "/person".nest { @@ -754,7 +755,7 @@ We can further improve by using the `nest` method together with `accept`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RouterFunction route = route() .path("/person", b1 -> b1 @@ -767,7 +768,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val route = coRouter { "/person".nest { @@ -782,6 +783,70 @@ Kotlin:: ====== +[[webflux-fn-serving-resources]] +== Serving Resources + +WebFlux.fn provides built-in support for serving resources. + +NOTE: In addition to the capabilities described below, it is possible to implement even more flexible resource handling thanks to +{spring-framework-api}++/web/reactive/function/server/RouterFunctions.html#resources(java.util.function.Function)++[`RouterFunctions#resource(java.util.function.Function)`]. + +[[webflux-fn-resource]] +=== Redirecting to a resource + +It is possible to redirect requests matching a specified predicate to a resource. This can be useful, for example, +for handling redirects in Single Page Applications. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ClassPathResource index = new ClassPathResource("static/index.html"); + RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate(); + RouterFunction redirectToIndex = route() + .resource(spaPredicate, index) + .build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val redirectToIndex = router { + val index = ClassPathResource("static/index.html") + val spaPredicate = !(path("/api/**") or path("/error")) + resource(spaPredicate, index) + } +---- +====== + +[[webflux-fn-resources]] +=== Serving resources from a root location + +It is also possible to route requests that match a given pattern to resources relative to a given root location. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + 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) } +---- +====== + + [[webflux-fn-running]] == Running a Server [.small]#xref:web/webmvc-functional.adoc#webmvc-fn-running[See equivalent in the Servlet stack]# @@ -821,7 +886,7 @@ xref:web/webflux/dispatcher-handler.adoc[DispatcherHandler] for how to run it): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -858,7 +923,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -909,7 +974,7 @@ For instance, consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RouterFunction route = route() .path("/person", b1 -> b1 @@ -928,7 +993,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val route = router { "/person".nest { @@ -964,7 +1029,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- SecurityManager securityManager = ... @@ -987,7 +1052,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val securityManager: SecurityManager = ... diff --git a/framework-docs/modules/ROOT/pages/web/webflux-reactive-libraries.adoc b/framework-docs/modules/ROOT/pages/web/webflux-reactive-libraries.adoc index 45ac4fcd581d..0e7253dae526 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-reactive-libraries.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-reactive-libraries.adoc @@ -4,26 +4,17 @@ `spring-webflux` depends on `reactor-core` and uses it internally to compose asynchronous logic and to provide Reactive Streams support. Generally, WebFlux APIs return `Flux` or `Mono` (since those are used internally) and leniently accept any Reactive Streams -`Publisher` implementation as input. The use of `Flux` versus `Mono` is important, because -it helps to express cardinality -- for example, whether a single or multiple asynchronous -values are expected, and that can be essential for making decisions (for example, when -encoding or decoding HTTP messages). +`Publisher` implementation as input. +When a `Publisher` is provided, it can be treated only as a stream with unknown semantics (0..N). +If, however, the semantics are known, you should wrap it with `Flux` or `Mono.from(Publisher)` instead +of passing the raw `Publisher`. +The use of `Flux` versus `Mono` is important, because it helps to express cardinality -- +for example, whether a single or multiple asynchronous values are expected, +and that can be essential for making decisions (for example, when encoding or decoding HTTP messages). For annotated controllers, WebFlux transparently adapts to the reactive library chosen by the application. This is done with the help of the -{api-spring-framework}/core/ReactiveAdapterRegistry.html[`ReactiveAdapterRegistry`], which +{spring-framework-api}/core/ReactiveAdapterRegistry.html[`ReactiveAdapterRegistry`], which provides pluggable support for reactive library and other asynchronous types. The registry has built-in support for RxJava 3, Kotlin coroutines and SmallRye Mutiny, but you can register others, too. - -For functional APIs (such as <>, the `WebClient`, and others), the general rules -for WebFlux APIs apply -- `Flux` and `Mono` as return values and a Reactive Streams -`Publisher` as input. When a `Publisher`, whether custom or from another reactive library, -is provided, it can be treated only as a stream with unknown semantics (0..N). If, however, -the semantics are known, you can wrap it with `Flux` or `Mono.from(Publisher)` instead -of passing the raw `Publisher`. - -For example, given a `Publisher` that is not a `Mono`, the Jackson JSON message writer -expects multiple values. If the media type implies an infinite stream (for example, -`application/json+stream`), values are written and flushed individually. Otherwise, -values are buffered into a list and rendered as a JSON array. \ 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 491a064f3ffd..eb64696b1d6e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-view.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-view.adoc @@ -1,11 +1,19 @@ [[webflux-view]] = View Technologies + [.small]#xref:web/webmvc-view.adoc[See equivalent in the Servlet stack]# -The use of view technologies in Spring WebFlux is pluggable. Whether you decide to +The rendering of views in Spring WebFlux is pluggable. Whether you decide to use Thymeleaf, FreeMarker, or some other view technology is primarily a matter of a configuration change. This chapter covers the view technologies integrated with Spring -WebFlux. We assume you are already familiar with xref:web/webflux/dispatcher-handler.adoc#webflux-viewresolution[View Resolution]. +WebFlux. + +For more context on view rendering, please see xref:web/webflux/dispatcher-handler.adoc#webflux-viewresolution[View Resolution]. + +WARNING: The views of a Spring WebFlux application live within internal trust boundaries +of the application. Views have access to beans in the application context, and as +such, we do not recommend use the Spring WebFlux template support in applications where +the templates are editable by external sources, since this can have security implications. @@ -51,7 +59,7 @@ The following example shows how to configure FreeMarker as a view technology: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -75,7 +83,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -116,7 +124,7 @@ a `java.util.Properties` object, and the `freemarkerVariables` property requires ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -139,7 +147,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -209,7 +217,7 @@ sections of the Spring MVC documentation. The Spring Framework has a built-in integration for using Spring WebFlux with any templating library that can run on top of the -https://www.jcp.org/en/jsr/detail?id=223[JSR-223] Java scripting engine. +{JSR}223[JSR-223] Java scripting engine. The following table shows the templating libraries that we have tested on different script engines: [%header] @@ -217,11 +225,11 @@ 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] |https://kotlinlang.org/[Kotlin] +|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin] |=== TIP: The basic rule for integrating any other script engine is that it must implement the @@ -261,7 +269,7 @@ The following example uses Mustache templates and the Nashorn JavaScript engine: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -286,7 +294,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -312,7 +320,7 @@ The `render` function is called with the following parameters: * `String template`: The template content * `Map model`: The view model * `RenderingContext renderingContext`: The - {api-spring-framework}/web/servlet/view/script/RenderingContext.html[`RenderingContext`] + {spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`] that gives access to the application context, the locale, the template loader, and the URL (since 5.0) @@ -329,7 +337,7 @@ The following example shows how to set a custom render function: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -354,7 +362,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -404,13 +412,88 @@ The following example shows how compile a template: ---- Check out the Spring Framework unit tests, -{spring-framework-main-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and -{spring-framework-main-code}/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script[resources], +{spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and +{spring-framework-code}/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script[resources], for more configuration examples. +[[webflux-view-fragments]] +== HTML Fragment +[.small]#xref:web/webmvc-view/mvc-fragments.adoc[See equivalent in the Servlet stack]# + +https://htmx.org/[HTMX] and https://turbo.hotwired.dev/[Hotwire Turbo] emphasize an +HTML-over-the-wire approach where clients receive server updates in HTML rather than in JSON. +This allows the benefits of an SPA (single page app) without having to write much or even +any JavaScript. For a good overview and to learn more, please visit their respective +websites. + +In Spring WebFlux, view rendering typically involves specifying one view and one model. +However, in HTML-over-the-wire a common capability is to send multiple HTML fragments that +the browser can use to update different parts of the page. For this, controller methods +can return `Collection`. For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + List handle() { + return List.of(Fragment.create("posts"), Fragment.create("comments")); + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + fun handle(): List { + return listOf(Fragment.create("posts"), Fragment.create("comments")) + } +---- +====== + +The same can be done also by returning the dedicated type `FragmentsRendering`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + FragmentsRendering handle() { + return FragmentsRendering.fragment("posts").fragment("comments").build(); + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + fun handle(): FragmentsRendering { + return FragmentsRendering.fragment("posts").fragment("comments").build() + } +---- +====== + +Each fragment can have an independent model, and that model inherits attributes from the +shared model for the request. + +HTMX and Hotwire Turbo support streaming updates over SSE (server-sent events). +A controller can create `FragmentsRendering` with a `Flux`, or with any other +reactive producer adaptable to a Reactive Streams `Publisher` via `ReactiveAdapterRegistry`. +It is also possible to return `Flux` directly without the `FragmentsRendering` +wrapper. + + + + [[webflux-view-httpmessagewriter]] == JSON and XML [.small]#xref:web/webmvc-view/mvc-jackson.adoc[See equivalent in the Servlet stack]# diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc index effa703ab605..448ff3db92d8 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc @@ -12,8 +12,8 @@ decode request and response content on the server side. `WebClient` needs an HTTP client library to perform requests with. There is built-in support for the following: -* https://github.com/reactor/reactor-netty[Reactor Netty] -* https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html[JDK HttpClient] +* {reactor-github-org}/reactor-netty[Reactor Netty] +* {java-api}/java.net.http/java/net/http/HttpClient.html[JDK HttpClient] * https://github.com/jetty-project/jetty-reactive-httpclient[Jetty Reactive HttpClient] * https://hc.apache.org/index.html[Apache HttpComponents] * Others can be plugged via `ClientHttpConnector`. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-attributes.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-attributes.adoc index 1683021b269b..df6e89419a6d 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-attributes.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-attributes.adoc @@ -9,7 +9,7 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient client = WebClient.builder() .filter((request, next) -> { @@ -28,7 +28,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = WebClient.builder() .filter { request, _ -> 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 4419eaa296fe..bd52881b4591 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 @@ -8,7 +8,7 @@ like `Mono` or Kotlin Coroutines `Deferred` as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono personMono = ... ; @@ -22,7 +22,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val personDeferred: Deferred = ... @@ -41,7 +41,7 @@ You can also have a stream of objects be encoded, as the following example shows ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Flux personFlux = ... ; @@ -55,7 +55,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val people: Flow = ... @@ -75,7 +75,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Person person = ... ; @@ -89,7 +89,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val person: Person = ... @@ -115,7 +115,7 @@ content is automatically set to `application/x-www-form-urlencoded` by the ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- MultiValueMap formData = ... ; @@ -128,7 +128,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val formData: MultiValueMap = ... @@ -146,7 +146,7 @@ You can also supply form data in-line by using `BodyInserters`, as the following ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import static org.springframework.web.reactive.function.BodyInserters.*; @@ -159,7 +159,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.reactive.function.BodyInserters.* @@ -185,7 +185,7 @@ multipart request. The following example shows how to create a `MultiValueMap result = webClient @@ -320,7 +320,7 @@ Mono result = webClient Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- var resource: Resource = ... var result: Mono = webClient 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 3d326fadfd32..53a2fc247cb8 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 @@ -25,7 +25,7 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient client = WebClient.builder() .codecs(configurer -> ... ) @@ -34,7 +34,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val webClient = WebClient.builder() .codecs { configurer -> ... } @@ -49,7 +49,7 @@ modified copy as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient client1 = WebClient.builder() .filter(filterA).filter(filterB).build(); @@ -64,7 +64,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client1 = WebClient.builder() .filter(filterA).filter(filterB).build() @@ -95,7 +95,7 @@ To change the limit for default codecs, use the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient webClient = WebClient.builder() .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) @@ -104,7 +104,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val webClient = WebClient.builder() .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) } @@ -123,7 +123,7 @@ To customize Reactor Netty settings, provide a pre-configured `HttpClient`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...); @@ -134,7 +134,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val httpClient = HttpClient.create().secure { ... } @@ -165,7 +165,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean public ReactorResourceFactory reactorResourceFactory() { @@ -175,7 +175,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean fun reactorResourceFactory() = ReactorResourceFactory() @@ -192,7 +192,7 @@ instances use shared resources, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean public ReactorResourceFactory resourceFactory() { @@ -220,7 +220,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean fun resourceFactory() = ReactorResourceFactory().apply { @@ -255,7 +255,7 @@ To configure a connection timeout: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import io.netty.channel.ChannelOption; @@ -269,7 +269,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import io.netty.channel.ChannelOption @@ -288,7 +288,7 @@ To configure a read or write timeout: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.handler.timeout.WriteTimeoutHandler; @@ -304,7 +304,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import io.netty.handler.timeout.ReadTimeoutHandler import io.netty.handler.timeout.WriteTimeoutHandler @@ -325,7 +325,7 @@ To configure a response timeout for all requests: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpClient httpClient = HttpClient.create() .responseTimeout(Duration.ofSeconds(2)); @@ -335,7 +335,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val httpClient = HttpClient.create() .responseTimeout(Duration.ofSeconds(2)); @@ -350,7 +350,7 @@ To configure a response timeout for a specific request: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient.create().get() .uri("https://example.org/path") @@ -364,7 +364,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- WebClient.create().get() .uri("https://example.org/path") @@ -388,7 +388,7 @@ The following example shows how to customize the JDK `HttpClient`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpClient httpClient = HttpClient.newBuilder() .followRedirects(Redirect.NORMAL) @@ -403,7 +403,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val httpClient = HttpClient.newBuilder() .followRedirects(Redirect.NORMAL) @@ -428,7 +428,7 @@ The following example shows how to customize Jetty `HttpClient` settings: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpClient httpClient = new HttpClient(); httpClient.setCookieStore(...); @@ -440,7 +440,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val httpClient = HttpClient() httpClient.cookieStore = ... @@ -465,7 +465,7 @@ shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean public JettyResourceFactory resourceFactory() { @@ -489,7 +489,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean fun resourceFactory() = JettyResourceFactory() @@ -521,7 +521,7 @@ The following example shows how to customize Apache HttpComponents `HttpClient` ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom(); clientBuilder.setDefaultRequestConfig(...); @@ -534,7 +534,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = HttpAsyncClients.custom().apply { setDefaultRequestConfig(...) diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-context.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-context.adoc index 749517ae205a..eb2619849e4a 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-context.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-context.adoc @@ -3,8 +3,8 @@ xref:web/webflux-webclient/client-attributes.adoc[Attributes] provide a convenient way to pass information to the filter chain but they only influence the current request. If you want to pass information that -propagates to additional requests that are nested, e.g. via `flatMap`, or executed after, -e.g. via `concatMap`, then you'll need to use the Reactor `Context`. +propagates to additional requests that are nested, for example, via `flatMap`, or executed after, +for example, via `concatMap`, then you'll need to use the Reactor `Context`. The Reactor `Context` needs to be populated at the end of a reactive chain in order to apply to all operations. For example: @@ -13,7 +13,7 @@ apply to all operations. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient client = WebClient.builder() .filter((request, next) -> diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-exchange.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-exchange.adoc index 83ddb7f3a88d..fa14363330f7 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-exchange.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-exchange.adoc @@ -9,7 +9,7 @@ depending on the response status: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono entityMono = client.get() .uri("/persons/1") @@ -27,7 +27,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val entity = client.get() .uri("/persons/1") 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 c1bd622687c0..d63ed06a9fb8 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 @@ -8,7 +8,7 @@ in order to intercept and modify requests, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient client = WebClient.builder() .filter((request, next) -> { @@ -24,7 +24,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = WebClient.builder() .filter { request, next -> @@ -46,7 +46,7 @@ a filter for basic authentication through a static factory method: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; @@ -57,7 +57,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication @@ -74,7 +74,7 @@ in a new `WebClient` instance that does not affect the original one. For example ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; @@ -87,7 +87,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = webClient.mutate() .filters { it.add(0, basicAuthentication("user", "password")) } @@ -107,7 +107,7 @@ any response content, whether expected or not, is released: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public ExchangeFilterFunction renewTokenFilter() { return (request, next) -> next.exchange(request).flatMap(response -> { @@ -127,7 +127,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun renewTokenFilter(): ExchangeFilterFunction? { return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction -> @@ -156,7 +156,7 @@ a custom filter class that helps with computing a `Content-Length` header for `P ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MultipartExchangeFilterFunction implements ExchangeFilterFunction { @@ -191,7 +191,7 @@ public class MultipartExchangeFilterFunction implements ExchangeFilterFunction { Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MultipartExchangeFilterFunction : ExchangeFilterFunction { diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-retrieve.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-retrieve.adoc index 28cb417588a7..256b8cc934c7 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-retrieve.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-retrieve.adoc @@ -7,7 +7,7 @@ The `retrieve()` method can be used to declare how to extract the response. For ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient client = WebClient.create("https://example.org"); @@ -19,7 +19,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = WebClient.create("https://example.org") @@ -36,7 +36,7 @@ Or to get only the body: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient client = WebClient.create("https://example.org"); @@ -48,7 +48,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = WebClient.create("https://example.org") @@ -65,7 +65,7 @@ To get a stream of decoded objects: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Flux result = client.get() .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) @@ -75,7 +75,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val result = client.get() .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) @@ -92,25 +92,25 @@ responses, use `onStatus` handlers as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .retrieve() - .onStatus(HttpStatus::is4xxClientError, response -> ...) - .onStatus(HttpStatus::is5xxServerError, response -> ...) + .onStatus(HttpStatusCode::is4xxClientError, response -> ...) + .onStatus(HttpStatusCode::is5xxServerError, response -> ...) .bodyToMono(Person.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .retrieve() - .onStatus(HttpStatus::is4xxClientError) { ... } - .onStatus(HttpStatus::is5xxServerError) { ... } + .onStatus(HttpStatusCode::is4xxClientError) { ... } + .onStatus(HttpStatusCode::is5xxServerError) { ... } .awaitBody() ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-synchronous.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-synchronous.adoc index 7f9c4a0e4ffc..dcc2ddc5ae01 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-synchronous.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-synchronous.adoc @@ -7,7 +7,7 @@ ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Person person = client.get().uri("/person/{id}", i).retrieve() .bodyToMono(Person.class) @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val person = runBlocking { client.get().uri("/person/{id}", i).retrieve() @@ -43,7 +43,7 @@ response individually, and instead wait for the combined result: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono personMono = client.get().uri("/person/{id}", personId) .retrieve().bodyToMono(Person.class); @@ -62,7 +62,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val data = runBlocking { val personDeferred = async { 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 8b1393cc52b6..febbb5498272 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 @@ -5,7 +5,7 @@ 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 -{spring-framework-main-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`] +{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`] sample in the OkHttp repository. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc index 204c5f771fe8..1e7f397c19b6 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc @@ -1,5 +1,6 @@ [[webflux-websocket]] = WebSockets + [.small]#xref:web/websocket.adoc[See equivalent in the Servlet stack]# This part of the reference documentation covers support for reactive-stack WebSocket @@ -27,7 +28,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.reactive.socket.WebSocketHandler; import org.springframework.web.reactive.socket.WebSocketSession; @@ -43,7 +44,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.reactive.socket.WebSocketHandler import org.springframework.web.reactive.socket.WebSocketSession @@ -63,7 +64,7 @@ Then you can map it to a URL: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig { @@ -81,7 +82,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig { @@ -105,7 +106,7 @@ further to do, or otherwise if not using the WebFlux config you'll need to decla ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig { @@ -121,7 +122,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig { @@ -177,7 +178,7 @@ following example shows such an implementation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class ExampleHandler implements WebSocketHandler { @@ -201,7 +202,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleHandler : WebSocketHandler { @@ -235,7 +236,7 @@ The following implementation combines the inbound and outbound streams: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class ExampleHandler implements WebSocketHandler { @@ -261,7 +262,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleHandler : WebSocketHandler { @@ -293,7 +294,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class ExampleHandler implements WebSocketHandler { @@ -322,7 +323,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleHandler : WebSocketHandler { @@ -396,7 +397,7 @@ not using the WebFlux config, use the below: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig { @@ -417,7 +418,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig { @@ -472,7 +473,7 @@ methods: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebSocketClient client = new ReactorNettyWebSocketClient(); @@ -485,7 +486,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = ReactorNettyWebSocketClient() diff --git a/framework-docs/modules/ROOT/pages/web/webflux.adoc b/framework-docs/modules/ROOT/pages/web/webflux.adoc index a9487c93739b..cbf487481cb8 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux.adoc @@ -7,12 +7,12 @@ The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports -https://www.reactive-streams.org/[Reactive Streams] back pressure, and runs on such servers as +{reactive-streams-site}/[Reactive Streams] back pressure, and runs on such servers as Netty, Undertow, and Servlet containers. Both web frameworks mirror the names of their source modules -({spring-framework-main-code}/spring-webmvc[spring-webmvc] and -{spring-framework-main-code}/spring-webflux[spring-webflux]) and co-exist side by side in the +({spring-framework-code}/spring-webmvc[spring-webmvc] and +{spring-framework-code}/spring-webflux[spring-webflux]) and co-exist side by side in the Spring Framework. Each module is optional. Applications can use one or the other module or, in some cases, both -- for example, Spring MVC controllers with the reactive `WebClient`. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc index d8fb4a7291c2..c3481a3e5d11 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc @@ -5,14 +5,14 @@ A common requirement for REST services is to include details in the body of error responses. The Spring Framework supports the "Problem Details for HTTP APIs" -specification, https://www.rfc-editor.org/rfc/rfc7807.html[RFC 7807]. +specification, {rfc-site}/rfc9457.html[RFC 9457]. The following are the main abstractions for this support: -- `ProblemDetail` -- representation for an RFC 7807 problem detail; a simple container +- `ProblemDetail` -- representation for an RFC 9457 problem detail; a simple container for both standard fields defined in the spec, and for non-standard ones. - `ErrorResponse` -- contract to expose HTTP error response details including HTTP -status, response headers, and a body in the format of RFC 7807; this allows exceptions to +status, response headers, and a body in the format of RFC 9457; this allows exceptions to encapsulate and expose the details of how they map to an HTTP response. All Spring WebFlux exceptions implement this. - `ErrorResponseException` -- basic `ErrorResponse` implementation that others @@ -28,7 +28,7 @@ and any `ErrorResponseException`, and renders an error response with a body. [.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-render[See equivalent in the Servlet stack]# You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from -any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows: +any `@RequestMapping` method to render an RFC 9457 response. This is processed as follows: - The `status` property of `ProblemDetail` determines the HTTP status. - The `instance` property of `ProblemDetail` is set from the current URL path, if not @@ -37,20 +37,24 @@ already set. "application/problem+json" over "application/json" when rendering a `ProblemDetail`, and also falls back on it if no compatible media type is found. -To enable RFC 7807 responses for Spring WebFlux exceptions and for any +To enable RFC 9457 responses for Spring WebFlux exceptions and for any `ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an xref:web/webflux/controller/ann-advice.adoc[@ControllerAdvice] in Spring configuration. The handler has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which includes all built-in web exceptions. You can add more exception handling methods, and use a protected method to map any exception to a `ProblemDetail`. +You can register `ErrorResponse` interceptors through the +xref:web/webflux/config.adoc[WebFlux Config] with a `WebFluxConfigurer`. Use that to intercept +any RFC 9457 response and take some action. + [[webflux-ann-rest-exceptions-non-standard]] == Non-Standard Fields [.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-non-standard[See equivalent in the Servlet stack]# -You can extend an RFC 7807 response with non-standard fields in one of two ways. +You can extend an RFC 9457 response with non-standard fields in one of two ways. One, insert into the "properties" `Map` of `ProblemDetail`. When using the Jackson library, the Spring Framework registers `ProblemDetailJacksonMixin` that ensures this @@ -60,7 +64,7 @@ this `Map`. You can also extend `ProblemDetail` to add dedicated non-standard properties. The copy constructor in `ProblemDetail` allows a subclass to make it easy to be created -from an existing `ProblemDetail`. This could be done centrally, e.g. from an +from an existing `ProblemDetail`. This could be done centrally, for example, from an `@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the `ProblemDetail` of an exception into a subclass with the additional non-standard fields. @@ -103,7 +107,7 @@ Message codes and arguments for each error are also resolved via `MessageSource` | `MissingRequestValueException` | (default) -| `+{0}+` a label for the value (e.g. "request header", "cookie value", ...), `+{1}+` the value name +| `+{0}+` a label for the value (for example, "request header", "cookie value", ...), `+{1}+` the value name | `NotAcceptableStatusException` | (default) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc b/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc index 5da8d201aedf..6fe3f94de44c 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc @@ -19,14 +19,14 @@ This section describes the HTTP caching related options available in Spring WebF == `CacheControl` [.small]#xref:web/webmvc/mvc-caching.adoc#mvc-caching-cachecontrol[See equivalent in the Servlet stack]# -{api-spring-framework}/http/CacheControl.html[`CacheControl`] provides support for +{spring-framework-api}/http/CacheControl.html[`CacheControl`] provides support for configuring settings related to the `Cache-Control` header and is accepted as an argument in a number of places: * xref:web/webflux/caching.adoc#webflux-caching-etag-lastmodified[Controllers] * xref:web/webflux/caching.adoc#webflux-caching-static-resources[Static Resources] -While https://tools.ietf.org/html/rfc7234#section-5.2.2[RFC 7234] describes all possible +While {rfc-site}/rfc7234#section-5.2.2[RFC 7234] describes all possible directives for the `Cache-Control` response header, the `CacheControl` type takes a use case-oriented approach that focuses on the common scenarios, as the following example shows: @@ -34,7 +34,7 @@ use case-oriented approach that focuses on the common scenarios, as the followin ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Cache for an hour - "Cache-Control: max-age=3600" CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS); @@ -50,7 +50,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Cache for an hour - "Cache-Control: max-age=3600" val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS) @@ -82,7 +82,7 @@ settings to a `ResponseEntity`, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/book/{id}") public ResponseEntity showBook(@PathVariable Long id) { @@ -100,7 +100,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/book/{id}") fun showBook(@PathVariable id: Long): ResponseEntity { @@ -130,7 +130,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RequestMapping public String myHandleMethod(ServerWebExchange exchange, Model model) { @@ -151,7 +151,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RequestMapping fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? { diff --git a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc index 69e2f8195784..c67851c129cf 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc @@ -26,7 +26,7 @@ You can use the `@EnableWebFlux` annotation in your Java config, as the followin ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -36,7 +36,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -44,6 +44,11 @@ Kotlin:: ---- ====== +NOTE: When using Spring Boot, you may want to use `@Configuration` classes of type `WebFluxConfigurer` but without +`@EnableWebFlux` to keep Spring Boot WebFlux customizations. See more details in +xref:#webflux-config-customize[the WebFlux config API section] and in +{spring-boot-docs-ref}/web/reactive.html#web.reactive.webflux.auto-configuration[the dedicated Spring Boot documentation]. + The preceding example registers a number of Spring WebFlux xref:web/webflux/dispatcher-handler.adoc#webflux-special-bean-types[infrastructure beans] and adapts to dependencies available on the classpath -- for JSON, XML, and others. @@ -61,10 +66,9 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { // Implement configuration methods... @@ -73,10 +77,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration -@EnableWebFlux class WebConfig : WebFluxConfigurer { // Implement configuration methods... @@ -91,7 +94,8 @@ class WebConfig : WebFluxConfigurer { [.small]#xref:web/webmvc/mvc-config/conversion.adoc[See equivalent in the Servlet stack]# By default, formatters for various number and date types are installed, along with support -for customization via `@NumberFormat` and `@DateTimeFormat` on fields. +for customization via `@NumberFormat`, `@DurationFormat`, and `@DateTimeFormat` on fields +and parameters. To register custom formatters and converters in Java config, use the following: @@ -99,10 +103,9 @@ To register custom formatters and converters in Java config, use the following: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -115,10 +118,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun addFormatters(registry: FormatterRegistry) { @@ -137,10 +139,9 @@ in the HTML spec. For such cases date and time formatting can be customized as f ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -154,10 +155,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun addFormatters(registry: FormatterRegistry) { @@ -191,10 +191,9 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -207,10 +206,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun getValidator(): Validator { @@ -228,7 +226,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class MyController { @@ -243,7 +241,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class MyController { @@ -276,10 +274,9 @@ The following example shows how to customize the requested content type resoluti ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -291,10 +288,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) { @@ -316,10 +312,9 @@ The following example shows how to customize how the request and response body a ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -331,14 +326,13 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { - // ... + configurer.defaultCodecs().maxInMemorySize(512 * 1024) } } ---- @@ -348,18 +342,17 @@ Kotlin:: more readers and writers, customize the default ones, or replace the default ones completely. For Jackson JSON and XML, consider using -{api-spring-framework}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`], +{spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`], which customizes Jackson's default properties with the following ones: -* https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled. -* https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled. +* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled. +* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled. It also automatically registers the following well-known modules if they are detected on the classpath: -* https://github.com/FasterXML/jackson-datatype-joda[`jackson-datatype-joda`]: Support for Joda-Time types. -* https://github.com/FasterXML/jackson-datatype-jsr310[`jackson-datatype-jsr310`]: Support for Java 8 Date and Time API types. -* https://github.com/FasterXML/jackson-datatype-jdk8[`jackson-datatype-jdk8`]: Support for other Java 8 types, such as `Optional`. -* https://github.com/FasterXML/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes. +* {jackson-github-org}/jackson-datatype-jsr310[`jackson-datatype-jsr310`]: Support for Java 8 Date and Time API types. +* {jackson-github-org}/jackson-datatype-jdk8[`jackson-datatype-jdk8`]: Support for other Java 8 types, such as `Optional`. +* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes. @@ -373,10 +366,9 @@ The following example shows how to configure view resolution: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -388,10 +380,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun configureViewResolvers(registry: ViewResolverRegistry) { @@ -409,10 +400,9 @@ underlying FreeMarker view technology): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @@ -434,10 +424,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun configureViewResolvers(registry: ViewResolverRegistry) { @@ -460,10 +449,9 @@ You can also plug in any `ViewResolver` implementation, as the following example ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @@ -477,10 +465,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun configureViewResolvers(registry: ViewResolverRegistry) { @@ -500,10 +487,9 @@ xref:web/webflux/reactive-spring.adoc#webflux-codecs[Codecs] from `spring-web`. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @@ -521,10 +507,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { @@ -549,7 +534,7 @@ See xref:web/webflux-view.adoc[View Technologies] for more on the view technolog [.small]#xref:web/webmvc/mvc-config/static-resources.adoc[See equivalent in the Servlet stack]# This option provides a convenient way to serve static resources from a list of -{api-spring-framework}/core/io/Resource.html[`Resource`]-based locations. +{spring-framework-api}/core/io/Resource.html[`Resource`]-based locations. In the next example, given a request that starts with `/resources`, the relative path is used to find and serve static resources relative to `/static` on the classpath. Resources @@ -562,10 +547,9 @@ the example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -580,10 +564,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun addResourceHandlers(registry: ResourceHandlerRegistry) { @@ -598,8 +581,8 @@ Kotlin:: See also xref:web/webflux/caching.adoc#webflux-caching-static-resources[HTTP caching support for static resources]. The resource handler also supports a chain of -{api-spring-framework}/web/reactive/resource/ResourceResolver.html[`ResourceResolver`] implementations and -{api-spring-framework}/web/reactive/resource/ResourceTransformer.html[`ResourceTransformer`] implementations, +{spring-framework-api}/web/reactive/resource/ResourceResolver.html[`ResourceResolver`] implementations and +{spring-framework-api}/web/reactive/resource/ResourceTransformer.html[`ResourceTransformer`] implementations, which can be used to create a toolchain for working with optimized resources. You can use the `VersionResourceResolver` for versioned resource URLs based on an MD5 hash @@ -613,10 +596,9 @@ The following example shows how to use `VersionResourceResolver` in your Java co ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -632,10 +614,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { override fun addResourceHandlers(registry: ResourceHandlerRegistry) { @@ -666,17 +647,16 @@ 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 -for fine-grained control, e.g. last-modified behavior and optimized resource resolution. +for fine-grained control, for example, last-modified behavior and optimized resource resolution. @@ -685,43 +665,10 @@ for fine-grained control, e.g. last-modified behavior and optimized resource res [.small]#xref:web/webmvc/mvc-config/path-matching.adoc[See equivalent in the Servlet stack]# You can customize options related to path matching. For details on the individual options, see the -{api-spring-framework}/web/reactive/config/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. +{spring-framework-api}/web/reactive/config/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. The following example shows how to use `PathMatchConfigurer`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebFlux - public class WebConfig implements WebFluxConfigurer { - - @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - configurer.addPathPrefix( - "/api", HandlerTypePredicate.forAnnotation(RestController.class)); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebFlux - class WebConfig : WebFluxConfigurer { - - @Override - fun configurePathMatch(configurer: PathMatchConfigurer) { - configurer.addPathPrefix( - "/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) - } - } ----- -====== +include-code::./WebConfig[] [TIP] ==== @@ -746,17 +693,16 @@ The WebFlux Java config allows you to customize blocking execution in WebFlux. You can have blocking controller methods called on a separate thread by providing an `AsyncTaskExecutor` such as the -{api-spring-framework}/core/task/VirtualThreadTaskExecutor.html[`VirtualThreadTaskExecutor`] +{spring-framework-api}/core/task/VirtualThreadTaskExecutor.html[`VirtualThreadTaskExecutor`] as follows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -769,10 +715,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { @Override @@ -807,10 +752,9 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -824,10 +768,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { @Override @@ -862,7 +805,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class WebConfig extends DelegatingWebFluxConfiguration { @@ -873,7 +816,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : DelegatingWebFluxConfiguration { diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller.adoc index 5db87830f5a5..14bbd8268200 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller.adoc @@ -14,7 +14,7 @@ The following listing shows a basic example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController public class HelloController { @@ -28,7 +28,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController class HelloController { diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-advice.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-advice.adoc index 362425369283..7f6f64be3502 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-advice.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-advice.adoc @@ -29,7 +29,7 @@ annotation, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Target all Controllers annotated with @RestController @ControllerAdvice(annotations = RestController.class) @@ -46,7 +46,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Target all Controllers annotated with @RestController @ControllerAdvice(annotations = [RestController::class]) @@ -64,6 +64,6 @@ Kotlin:: The selectors in the preceding example are evaluated at runtime and may negatively impact performance if used extensively. See the -{api-spring-framework}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] +{spring-framework-api}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] javadoc for more details. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-exceptions.adoc index c62341d365a7..a4435e11d5f5 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-exceptions.adoc @@ -7,43 +7,8 @@ `@ExceptionHandler` methods to handle exceptions from controller methods. The following example includes such a handler method: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Controller - public class SimpleController { - - // ... - - @ExceptionHandler // <1> - public ResponseEntity handle(IOException ex) { - // ... - } - } ----- -<1> Declaring an `@ExceptionHandler`. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Controller - class SimpleController { - - // ... - - @ExceptionHandler // <1> - fun handle(ex: IOException): ResponseEntity { - // ... - } - } ----- -<1> Declaring an `@ExceptionHandler`. -====== +include-code::./SimpleController[indent=0] The exception can match against a top-level exception being propagated (that is, a direct @@ -65,6 +30,22 @@ Support for `@ExceptionHandler` methods in Spring WebFlux is provided by the `HandlerAdapter` for `@RequestMapping` methods. See xref:web/webflux/dispatcher-handler.adoc[`DispatcherHandler`] for more detail. +[[webflux-ann-exceptionhandler-media]] +== Media Type Mapping +[.small]#xref:web/webmvc/mvc-controller/ann-exceptionhandler.adoc#mvc-ann-exceptionhandler-media[See equivalent in the Servlet stack]# + +In addition to exception types, `@ExceptionHandler` methods can also declare producible media types. +This allows to refine error responses depending on the media types requested by HTTP clients, typically in the "Accept" HTTP request header. + +Applications can declare producible media types directly on annotations, for the same exception type: + + +include-code::./MediaTypeController[tag=mediatype,indent=0] + +Here, methods handle the same exception type but will not be rejected as duplicates. +Instead, API clients requesting "application/json" will receive a JSON error, and browsers will get an HTML error view. +Each `@ExceptionHandler` annotation can declare several producible media types, +the content negotiation during the error handling phase will decide which content type will be used. [[webflux-ann-exceptionhandler-args]] diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-initbinder.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-initbinder.adoc index 3893881ea42d..d093ba9e3de9 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-initbinder.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-initbinder.adoc @@ -24,7 +24,7 @@ xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux config] to regist ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class FormController { @@ -43,7 +43,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class FormController { @@ -71,7 +71,7 @@ controller-specific `Formatter` instances, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class FormController { @@ -88,7 +88,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class FormController { diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/arguments.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/arguments.adoc index 87cb4522b2aa..37d297afe3ba 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/arguments.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/arguments.adoc @@ -77,7 +77,7 @@ and others) and is equivalent to `required=false`. | For access to a part in a `multipart/form-data` request. Supports reactive types. See xref:web/webflux/controller/ann-methods/multipart-forms.adoc[Multipart Content] and xref:web/webflux/reactive-spring.adoc#webflux-multipart[Multipart Data]. -| `java.util.Map`, `org.springframework.ui.Model`, and `org.springframework.ui.ModelMap`. +| `java.util.Map` or `org.springframework.ui.Model` | For access to the model that is used in HTML controllers and is exposed to templates as part of view rendering. @@ -89,9 +89,9 @@ and others) and is equivalent to `required=false`. Note that use of `@ModelAttribute` is optional -- for example, to set its attributes. See "`Any other argument`" later in this table. -| `Errors`, `BindingResult` +| `Errors` or `BindingResult` | For access to errors from validation and data binding for a command object, i.e. a - `@ModelAttribute` argument. An `Errors`, or `BindingResult` argument must be declared + `@ModelAttribute` argument. An `Errors` or `BindingResult` argument must be declared immediately after the validated method argument. | `SessionStatus` + class-level `@SessionAttributes` @@ -114,7 +114,7 @@ and others) and is equivalent to `required=false`. | Any other argument | If a method argument is not matched to any of the above, it is, by default, resolved as a `@RequestParam` if it is a simple type, as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], or as a `@ModelAttribute`, otherwise. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/cookievalue.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/cookievalue.adoc index 79b62352e7b7..49074543f082 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/cookievalue.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/cookievalue.adoc @@ -19,7 +19,7 @@ The following code sample demonstrates how to get the cookie value: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/demo") public void handle(@CookieValue("JSESSIONID") String cookie) { // <1> @@ -30,7 +30,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/demo") fun handle(@CookieValue("JSESSIONID") cookie: String) { // <1> diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/httpentity.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/httpentity.adoc index 1c9d77d8494e..5711095c0706 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/httpentity.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/httpentity.adoc @@ -11,7 +11,7 @@ container object that exposes request headers and the body. The following exampl ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") public void handle(HttpEntity entity) { @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") fun handle(entity: HttpEntity) { diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/jackson.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/jackson.adoc index fe3db7c7d560..9573aadfaa15 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/jackson.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/jackson.adoc @@ -8,7 +8,7 @@ Spring offers support for the Jackson JSON library. [.small]#xref:web/webmvc/mvc-controller/ann-methods/jackson.adoc[See equivalent in the Servlet stack]# Spring WebFlux provides built-in support for -https://www.baeldung.com/jackson-json-view-annotation[Jackson's Serialization Views], +{baeldung-blog}/jackson-json-view-annotation[Jackson's Serialization Views], which allows rendering only a subset of all fields in an `Object`. To use it with `@ResponseBody` or `ResponseEntity` controller methods, you can use Jackson's `@JsonView` annotation to activate a serialization view class, as the following example shows: @@ -17,7 +17,7 @@ which allows rendering only a subset of all fields in an `Object`. To use it wit ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController public class UserController { @@ -59,7 +59,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController class UserController { @@ -81,7 +81,7 @@ Kotlin:: ---- ====== -NOTE: `@JsonView` allows an array of view classes but you can only specify only one per +NOTE: `@JsonView` allows an array of view classes but you can specify only one per controller method. Use a composite interface if you need to activate multiple views. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/matrix-variables.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/matrix-variables.adoc index b5a5bd9753c5..805bbe54753c 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/matrix-variables.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/matrix-variables.adoc @@ -3,7 +3,7 @@ [.small]#xref:web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc[See equivalent in the Servlet stack]# -https://tools.ietf.org/html/rfc3986#section-3.3[RFC 3986] discusses name-value pairs in +{rfc-site}/rfc3986#section-3.3[RFC 3986] discusses name-value pairs in path segments. In Spring WebFlux, we refer to those as "`matrix variables`" based on an https://www.w3.org/DesignIssues/MatrixURIs.html["`old post`"] by Tim Berners-Lee, but they can be also be referred to as URI path parameters. @@ -23,7 +23,7 @@ variables are expected. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // GET /pets/42;q=11;r=22 @@ -37,7 +37,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // GET /pets/42;q=11;r=22 @@ -59,7 +59,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // GET /owners/42;q=11/pets/21;q=22 @@ -75,7 +75,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/owners/{ownerId}/pets/{petId}") fun findPet( @@ -95,7 +95,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // GET /pets/42 @@ -108,7 +108,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // GET /pets/42 @@ -126,7 +126,7 @@ To get all matrix variables, use a `MultiValueMap`, as the following example sho ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // GET /owners/42;q=11;r=12/pets/21;q=22;s=23 @@ -142,7 +142,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // GET /owners/42;q=11;r=12/pets/21;q=22;s=23 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 8cccf3b7d0d4..24999a0e1726 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 @@ -3,14 +3,14 @@ [.small]#xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Servlet stack]# -The `@ModelAttribute` method parameter annotation binds request parameters onto a model -object. For example: +The `@ModelAttribute` method parameter annotation binds form data, query parameters, +URI path variables, and request headers onto a model object. For example: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute Pet pet) { } // <1> @@ -19,7 +19,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") fun processSubmit(@ModelAttribute pet: Pet): String { } // <1> @@ -27,6 +27,10 @@ Kotlin:: <1> Bind to an instance of `Pet`. ====== +Form data and query parameters take precedence over URI variables and headers, which are +included only if they don't override request parameters with the same name. Dashes are +stripped from header names. + The `Pet` instance may be: * Accessed from the model where it could have been added by a @@ -54,7 +58,7 @@ When using constructor binding, you can customize request parameter names throug ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class Account { @@ -67,7 +71,7 @@ Java:: ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Account(@BindParam("first-name") val firstName: String) ---- @@ -77,7 +81,11 @@ NOTE: The `@BindParam` may also be placed on the fields that correspond to const parameters. While `@BindParam` is supported out of the box, you can also use a different annotation by setting a `DataBinder.NameResolver` on `DataBinder` -WebFlux, unlike Spring MVC, supports reactive types in the model, e.g. `Mono`. +Constructor binding supports `List`, `Map`, and array arguments either converted from +a single string, for example, comma-separated list, or based on indexed keys such as +`accounts[2].name` or `account[KEY].name`. + +WebFlux, unlike Spring MVC, supports reactive types in the model, for example, `Mono`. You can declare a `@ModelAttribute` argument with or without a reactive type wrapper, and it will be resolved accordingly to the actual value. @@ -89,7 +97,7 @@ in order to handle such errors in the controller method. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { <1> @@ -103,7 +111,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> @@ -124,7 +132,7 @@ directly through it. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public Mono processSubmit(@Valid @ModelAttribute("pet") Mono petMono) { @@ -140,7 +148,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono): Mono { @@ -164,7 +172,7 @@ xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1> @@ -178,7 +186,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> @@ -197,7 +205,10 @@ controller method xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation] TIP: Using `@ModelAttribute` is optional. By default, any argument that is not a simple value type as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty] -_AND_ that is not resolved by any other argument resolver is treated as an `@ModelAttribute`. - +{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty] +_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`. +WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute` +support described above does not allow proper ahead-of-time inference of related data +binding reflection hints. As a consequence, it is recommended to explicitly annotate +method parameters with `@ModelAttribute` for use in a GraalVM native image. 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 462bf3eccb1f..5616e8110b8f 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 @@ -13,7 +13,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class MyForm { @@ -38,7 +38,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyForm( val name: String, @@ -87,7 +87,7 @@ You can access individual parts with `@RequestPart`, as the following example sh ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") public String handle(@RequestPart("meta-data") Part metadata, // <1> @@ -100,7 +100,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") fun handle(@RequestPart("meta-data") Part metadata, // <1> @@ -122,7 +122,7 @@ you can declare a concrete target `Object`, instead of `Part`, as the following ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") public String handle(@RequestPart("meta-data") MetaData metadata) { // <1> @@ -133,7 +133,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") fun handle(@RequestPart("meta-data") metadata: MetaData): String { // <1> @@ -156,7 +156,7 @@ error related operators: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") public String handle(@Valid @RequestPart("meta-data") Mono metadata) { @@ -166,7 +166,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String { @@ -188,7 +188,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") public String handle(@RequestBody Mono> parts) { // <1> @@ -199,7 +199,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") fun handle(@RequestBody parts: MultiValueMap): String { // <1> @@ -230,7 +230,7 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") public void handle(@RequestBody Flux allPartsEvents) { <1> @@ -270,7 +270,7 @@ file upload. Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") fun handle(@RequestBody allPartsEvents: Flux) = { // <1> diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestattrib.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestattrib.adoc index d5359a575b3b..4fc08067bfb3 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestattrib.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestattrib.adoc @@ -11,7 +11,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/") public String handle(@RequestAttribute Client client) { <1> @@ -22,7 +22,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/") fun handle(@RequestAttribute client: Client): String { // <1> diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestbody.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestbody.adoc index b4b78afd28db..90d1ea2c83af 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestbody.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestbody.adoc @@ -11,7 +11,7 @@ The following example uses a `@RequestBody` argument: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") public void handle(@RequestBody Account account) { @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") fun handle(@RequestBody account: Account) { @@ -37,7 +37,7 @@ and fully non-blocking reading and (client-to-server) streaming. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") public void handle(@RequestBody Mono account) { @@ -47,7 +47,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") fun handle(@RequestBody accounts: Flow) { @@ -70,7 +70,7 @@ related operators: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") public void handle(@Valid @RequestBody Mono account) { @@ -80,7 +80,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") fun handle(@Valid @RequestBody account: Mono) { @@ -96,7 +96,7 @@ that case the request body must not be a `Mono`, and will be resolved first: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") public void handle(@Valid @RequestBody Account account, Errors errors) { @@ -106,7 +106,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") fun handle(@Valid @RequestBody account: Mono) { diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestheader.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestheader.adoc index e186391ef862..64c3fa9a7a9e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestheader.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestheader.adoc @@ -25,7 +25,7 @@ The following example gets the value of the `Accept-Encoding` and `Keep-Alive` h ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/demo") public void handle( @@ -39,7 +39,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/demo") fun handle( diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestparam.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestparam.adoc index 2752035758fd..0066245d26c6 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestparam.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestparam.adoc @@ -10,7 +10,7 @@ controller. The following code snippet shows the usage: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/pets") @@ -32,7 +32,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.ui.set @@ -74,7 +74,7 @@ When a `@RequestParam` annotation is declared on a `Map` or Note that use of `@RequestParam` is optional -- for example, to set its attributes. By default, any argument that is a simple value type (as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) +{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) and is not resolved by any other argument resolver is treated as if it were annotated with `@RequestParam`. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/responsebody.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/responsebody.adoc index d6befe47fdc7..d0d34f8d2e23 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/responsebody.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/responsebody.adoc @@ -11,7 +11,7 @@ example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/accounts/{id}") @ResponseBody @@ -22,7 +22,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/accounts/{id}") @ResponseBody diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/responseentity.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/responseentity.adoc index 00de86dad575..21766d338460 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/responseentity.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/responseentity.adoc @@ -9,7 +9,7 @@ ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/something") public ResponseEntity handle() { @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/something") fun handle(): ResponseEntity { 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 7752ee485377..22fe3570b6c6 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 @@ -4,9 +4,20 @@ [.small]#xref:web/webmvc/mvc-controller/ann-methods/return-types.adoc[See equivalent in the Servlet stack]# The following table shows the supported controller method return values. Note that reactive -types from libraries such as Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other] are +types from libraries such as Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other] are generally supported for all return values. +For return types like `Flux`, when multiple values are expected, elements are streamed as they come +and are not buffered. This is the default behavior, as keeping a potentially large amount of elements in memory +is not efficient. If the media type implies an infinite stream (for example, +`application/json+stream`), values are written and flushed individually. Otherwise, +values are written individually and the flushing happens separately. + +NOTE: If an error happens while an element is encoded to JSON, the response might have been written to and committed already +and it is impossible at that point to render a proper error response. +In some cases, applications can choose to trade memory efficiency for better handling such errors by buffering elements and encoding them all at once. +Controllers can then return a `Flux>`; Reactor provides a dedicated operator for that, `Flux#collectList()`. + [cols="1,2", options="header"] |=== | Controller method return value | Description @@ -24,11 +35,11 @@ generally supported for all return values. | For returning a response with headers and no body. | `ErrorResponse` -| To render an RFC 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] | `ProblemDetail` -| To render an RFC 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] | `String` @@ -57,6 +68,10 @@ generally supported for all return values. | `Rendering` | An API for model and view rendering scenarios. +| `FragmentsRendering`, `Flux`, `Collection` +| For rendering one or more fragments each with its own view and model. + See xref:web/webflux-view.adoc#webflux-view-fragments[HTML Fragments] for more details. + | `void` | A method with a `void`, possibly asynchronous (for example, `Mono`), return type (or a `null` return value) is considered to have fully handled the response if it also has a `ServerHttpResponse`, @@ -75,7 +90,7 @@ generally supported for all return values. | Other return values | If a return value remains unresolved in any other way, it is treated as a model attribute, unless it is a simple type as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], in which case it remains unresolved. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/sessionattribute.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/sessionattribute.adoc index 46ebcba9b49b..4469d226435b 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/sessionattribute.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/sessionattribute.adoc @@ -11,7 +11,7 @@ you can use the `@SessionAttribute` annotation on a method parameter, as the fol ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/") public String handle(@SessionAttribute User user) { // <1> @@ -22,7 +22,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/") fun handle(@SessionAttribute user: User): String { // <1> diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/sessionattributes.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/sessionattributes.adoc index d71db97849a7..7bc918c846fd 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/sessionattributes.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/sessionattributes.adoc @@ -15,7 +15,7 @@ Consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller @SessionAttributes("pet") <1> @@ -27,7 +27,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller @SessionAttributes("pet") // <1> @@ -47,7 +47,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller @SessionAttributes("pet") // <1> @@ -71,7 +71,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller @SessionAttributes("pet") // <1> 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 bf8d7afa04e9..7d79ea1d468e 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 @@ -28,7 +28,7 @@ The following example uses a `@ModelAttribute` method: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute public void populateModel(@RequestParam String number, Model model) { @@ -39,7 +39,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute fun populateModel(@RequestParam number: String, model: Model) { @@ -55,7 +55,7 @@ The following example adds one attribute only: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute public Account addAccount(@RequestParam String number) { @@ -65,7 +65,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute fun addAccount(@RequestParam number: String): Account { @@ -75,7 +75,7 @@ Kotlin:: ====== NOTE: When a name is not explicitly specified, a default name is chosen based on the type, -as explained in the javadoc for {api-spring-framework}/core/Conventions.html[`Conventions`]. +as explained in the javadoc for {spring-framework-api}/core/Conventions.html[`Conventions`]. You can always assign an explicit name by using the overloaded `addAttribute` method or through the name attribute on `@ModelAttribute` (for a return value). @@ -89,7 +89,7 @@ declared without a wrapper, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute public void addAccount(@RequestParam String number) { @@ -105,7 +105,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.ui.set @@ -137,7 +137,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/accounts/{id}") @ModelAttribute("myAccount") @@ -149,7 +149,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/accounts/{id}") @ModelAttribute("myAccount") 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 57203a2720ee..cbfcad7a7cec 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 @@ -28,13 +28,19 @@ because, arguably, most controller methods should be mapped to a specific HTTP m using `@RequestMapping`, which, by default, matches to all HTTP methods. At the same time, a `@RequestMapping` is still needed at the class level to express shared mappings. +NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping` +annotations that are declared on the same element (class, interface, or method). If +multiple `@RequestMapping` annotations are detected on the same element, a warning will +be logged, and only the first mapping will be used. This also applies to composed +`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc. + The following example uses type and method level mappings: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/persons") @@ -55,7 +61,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/persons") @@ -123,7 +129,7 @@ Captured URI variables can be accessed with `@PathVariable`, as the following ex ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/owners/{ownerId}/pets/{petId}") public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { @@ -133,7 +139,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/owners/{ownerId}/pets/{petId}") fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet { @@ -150,7 +156,7 @@ You can declare URI variables at the class and method levels, as the following e ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/owners/{ownerId}") // <1> @@ -167,7 +173,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/owners/{ownerId}") // <1> @@ -207,7 +213,7 @@ extracts the name, version, and file extension: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") public void handle(@PathVariable String version, @PathVariable String ext) { @@ -217,7 +223,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") fun handle(@PathVariable version: String, @PathVariable ext: String) { @@ -268,7 +274,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping(path = "/pets", consumes = "application/json") public void addPet(@RequestBody Pet pet) { @@ -278,7 +284,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/pets", consumes = ["application/json"]) fun addPet(@RequestBody pet: Pet) { @@ -309,7 +315,7 @@ content types that a controller method produces, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping(path = "/pets/{petId}", produces = "application/json") @ResponseBody @@ -320,7 +326,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/pets/{petId}", produces = ["application/json"]) @ResponseBody @@ -337,7 +343,7 @@ You can declare a shared `produces` attribute at the class level. Unlike most ot mapping attributes, however, when used at the class level, a method-level `produces` attribute overrides rather than extend the class level declaration. -TIP: `MediaType` provides constants for commonly used media types -- e.g. +TIP: `MediaType` provides constants for commonly used media types -- for example, `APPLICATION_JSON_VALUE`, `APPLICATION_XML_VALUE`. @@ -353,7 +359,7 @@ specific value (`myParam=myValue`). The following examples tests for a parameter ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping(path = "/pets/{petId}", params = "myParam=myValue") // <1> public void findPet(@PathVariable String petId) { @@ -364,7 +370,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/pets/{petId}", params = ["myParam=myValue"]) // <1> fun findPet(@PathVariable petId: String) { @@ -380,7 +386,7 @@ You can also use the same with request header conditions, as the following examp ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") // <1> public void findPet(@PathVariable String petId) { @@ -391,7 +397,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/pets/{petId}", headers = ["myHeader=myValue"]) // <1> fun findPet(@PathVariable petId: String) { @@ -436,8 +442,14 @@ attributes with a narrower, more specific purpose. `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, and `@PatchMapping` are examples of composed annotations. They are provided, because, arguably, most controller methods should be mapped to a specific HTTP method versus using `@RequestMapping`, -which, by default, matches to all HTTP methods. If you need an example of composed -annotations, look at how those are declared. +which, by default, matches to all HTTP methods. If you need an example of how to implement +a composed annotation, look at how those are declared. + +NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping` +annotations that are declared on the same element (class, interface, or method). If +multiple `@RequestMapping` annotations are detected on the same element, a warning will +be logged, and only the first mapping will be used. This also applies to composed +`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc. Spring WebFlux also supports custom request mapping attributes with custom request matching logic. This is a more advanced option that requires sub-classing @@ -457,7 +469,7 @@ under different URLs. The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class MyConfig { @@ -483,7 +495,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class MyConfig { @@ -509,12 +521,19 @@ Kotlin:: [[webflux-ann-httpexchange-annotation]] == `@HttpExchange` -[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-httpexchange-annotation[See equivalent in the Reactive stack]# - -As an alternative to `@RequestMapping`, you can also handle requests with `@HttpExchange` -methods. Such methods are declared on an -xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] and can be used as -a client via `HttpServiceProxyFactory` or implemented by a server `@Controller`. +[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-httpexchange-annotation[See equivalent in the Servlet stack]# + +While the main purpose of `@HttpExchange` is to abstract HTTP client code with a +generated proxy, the +xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] on which +such annotations are placed is a contract neutral to client vs server use. +In addition to simplifying client code, there are also cases where an HTTP Interface +may be a convenient way for servers to expose their API for client access. This leads +to increased coupling between client and server and is often not a good choice, +especially for public API's, but may be exactly the goal for an internal API. +It is an approach commonly used in Spring Cloud, and it is why `@HttpExchange` is +supported as an alternative to `@RequestMapping` for server side handling in +controller classes. For example: @@ -522,18 +541,25 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - @RestController @HttpExchange("/persons") - class PersonController { + interface PersonService { @GetExchange("/{id}") + Person getPerson(@PathVariable Long id); + + @PostExchange + void add(@RequestBody Person person); + } + + @RestController + class PersonController implements PersonService { + public Person getPerson(@PathVariable Long id) { // ... } - @PostExchange @ResponseStatus(HttpStatus.CREATED) public void add(@RequestBody Person person) { // ... @@ -543,32 +569,40 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - @RestController @HttpExchange("/persons") - class PersonController { + interface PersonService { @GetExchange("/{id}") - fun getPerson(@PathVariable id: Long): Person { + fun getPerson(@PathVariable id: Long): Person + + @PostExchange + fun add(@RequestBody person: Person) + } + + @RestController + class PersonController : PersonService { + + override fun getPerson(@PathVariable id: Long): Person { // ... } - @PostExchange @ResponseStatus(HttpStatus.CREATED) - fun add(@RequestBody person: Person) { + override fun add(@RequestBody person: Person) { // ... } } ---- ====== -There some differences between `@HttpExchange` and `@RequestMapping` since the -former needs to remain suitable for client and server use. For example, while -`@RequestMapping` can be declared to handle any number of paths and each path can -be a pattern, `@HttpExchange` must be declared with a single, concrete path. There are -also differences in the supported method parameters. Generally, `@HttpExchange` supports -a subset of method parameters that `@RequestMapping` does, excluding any parameters that -are server side only. For details see the list of supported method parameters for -xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[HTTP interface] and for -xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping]. +`@HttpExchange` and `@RequestMapping` have differences. +`@RequestMapping` can map to any number of requests by path patterns, HTTP methods, +and more, while `@HttpExchange` declares a single endpoint with a concrete HTTP method, +path, and content types. + +For method parameters and returns values, generally, `@HttpExchange` supports a +subset of the method parameters that `@RequestMapping` does. Notably, it excludes any +server-side specific parameter types. For details, see the list for +xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and +xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping]. \ No newline at end of file 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 8b0c069db657..e22e07b94bbe 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 @@ -3,30 +3,43 @@ [.small]#xref:web/webmvc/mvc-controller/ann-validation.adoc[See equivalent in the Servlet stack]# -Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] support for -`@RequestMapping` methods, including the option to use -xref:core/validation/beanvalidation.adoc[Java Bean Validation]. -The validation support works on two levels. +Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] for +`@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation]. +Validation may be applied at one of two levels: -First, method parameters such as -xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], +1. xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], xref:web/webflux/controller/ann-methods/requestbody.adoc[@RequestBody], and -xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] do perform -validation if annotated with Jakarta's `@Valid` or Spring's `@Validated` annotation, and -raise `MethodArgumentNotValidException` in case of validation errors. If you want to handle -the errors in the controller method instead, you can declare an `Errors` or `BindingResult` -method parameter immediately after the validated parameter. - -Second, if https://beanvalidation.org/[Java Bean Validation] is present _AND_ other method -parameters, e.g. `@RequestHeader`, `@RequestParam`, `@PathVariable` have `@Constraint` -annotations, then method validation is applied to all method arguments, raising -`HandlerMethodValidationException` in case of validation errors. You can still declare an -`Errors` or `BindingResult` after an `@Valid` method parameter, and handle validation -errors within the controller method, as long as there are no validation errors on other -method arguments. +xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] argument +resolvers validate a method argument individually if the method parameter is annotated +with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or +`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be +discussed next). The exception raised in this case is `WebExchangeBindException`. + +2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared +directly on method parameters, or on the method (for the return value), then method +validation must be applied, and that supersedes validation at the method argument level +because method validation covers both method parameter constraints and nested constraints +via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`. + +Applications must handle both `WebExchangeBindException` and +`HandlerMethodValidationException` as either may be raised depending on the controller +method signature. The two exceptions, however are designed to be very similar, and can be +handled with almost identical code. The main difference is that the former is for a single +object while the latter is for a list of method parameters. + +NOTE: `@Valid` is not a constraint annotation, but rather for nested constraints within +an Object. Therefore, by itself `@Valid` does not lead to method validation. `@NotNull` +on the other hand is a constraint, and adding it to an `@Valid` parameter leads to method +validation. For nullability specifically, you may also use the `required` flag of +`@RequestBody` or `@ModelAttribute`. + +Method validation may be used in combination with `Errors` or `BindingResult` method +parameters. However, the controller method is called only if all validation errors are on +method parameters with an `Errors` immediately after. If there are validation errors on +any other method parameter then `HandlerMethodValidationException` is raised. You can configure a `Validator` globally through the -xref:web/webflux/config.adoc#webflux-config-validation[WebMvc config], or locally +xref:web/webflux/config.adoc#webflux-config-validation[WebFlux config], or locally through an xref:web/webflux/controller/ann-initbinder.adoc[@InitBinder] method in an `@Controller` or `@ControllerAdvice`. You can also use multiple validators. @@ -36,15 +49,15 @@ through an AOP proxy. In order to take advantage of the Spring MVC built-in supp method validation added in Spring Framework 6.1, you need to remove the class level `@Validated` annotation from the controller. -The xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] section provides further -details on how `MethodArgumentNotValidException` and `HandlerMethodValidationException` +The xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] section provides further +details on how `WebExchangeBindException` and `HandlerMethodValidationException` are handled, and also how their rendering can be customized through a `MessageSource` and 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: @@ -52,7 +65,7 @@ methods by controller method parameter type: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HandlerMethodValidationException ex = ... ; @@ -82,7 +95,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // HandlerMethodValidationException val ex diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc index 93cc097b1577..00241404fca6 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc @@ -16,11 +16,11 @@ your Java configuration, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan("org.example.web") // <1> - public class WebConfig { + public class WebConfiguration { // ... } @@ -29,11 +29,11 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan("org.example.web") // <1> - class WebConfig { + class WebConfiguration { // ... } diff --git a/framework-docs/modules/ROOT/pages/web/webflux/dispatcher-handler.adoc b/framework-docs/modules/ROOT/pages/web/webflux/dispatcher-handler.adoc index b0919fd8fdab..8a17248f5ee1 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/dispatcher-handler.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/dispatcher-handler.adoc @@ -12,7 +12,7 @@ This model is flexible and supports diverse workflows. It is also designed to be a Spring bean itself and implements `ApplicationContextAware` for access to the context in which it runs. If `DispatcherHandler` is declared with a bean name of `webHandler`, it is, in turn, discovered by -{api-spring-framework}/web/server/adapter/WebHttpHandlerBuilder.html[`WebHttpHandlerBuilder`], +{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html[`WebHttpHandlerBuilder`], which puts together a request-processing chain, as described in xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API]. Spring configuration in a WebFlux application typically contains: @@ -29,7 +29,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ApplicationContext context = ... HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build(); @@ -37,7 +37,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val context: ApplicationContext = ... val handler = WebHttpHandlerBuilder.applicationContext(context).build() @@ -144,9 +144,9 @@ as a `HandlerResult`, along with some additional context, and passed to the firs | 100 | `ViewResolutionResultHandler` -| `CharSequence`, {api-spring-framework}/web/reactive/result/view/View.html[`View`], - {api-spring-framework}/ui/Model.html[Model], `Map`, - {api-spring-framework}/web/reactive/result/view/Rendering.html[Rendering], +| `CharSequence`, {spring-framework-api}/web/reactive/result/view/View.html[`View`], + {spring-framework-api}/ui/Model.html[Model], `Map`, + {spring-framework-api}/web/reactive/result/view/Rendering.html[Rendering], or any other `Object` is treated as a model attribute. See also xref:web/webflux/dispatcher-handler.adoc#webflux-viewresolution[View Resolution]. @@ -170,7 +170,7 @@ A `HandlerAdapter` may expose its exception handling mechanism as a A `HandlerAdapter` may also choose to implement `DispatchExceptionHandler`. In that case `DispatcherHandler` will apply it to exceptions that arise before a handler is mapped, -e.g. during handler mapping, or earlier, e.g. in a `WebFilter`. +for example, during handler mapping, or earlier, for example, in a `WebFilter`. See also xref:web/webflux/controller/ann-exceptions.adoc[Exceptions] in the "`Annotated Controller`" section or xref:web/webflux/reactive-spring.adoc#webflux-exception-handler[Exceptions] in the WebHandler API section. @@ -184,9 +184,11 @@ xref:web/webflux/reactive-spring.adoc#webflux-exception-handler[Exceptions] in t View resolution enables rendering to a browser with an HTML template and a model without tying you to a specific view technology. In Spring WebFlux, view resolution is supported through a dedicated xref:web/webflux/dispatcher-handler.adoc#webflux-resulthandling[HandlerResultHandler] that uses - `ViewResolver` instances to map a String (representing a logical view name) to a `View` +`ViewResolver` instances to map a String (representing a logical view name) to a `View` instance. The `View` is then used to render the response. +Web applications need to use a xref:web/webflux-view.adoc[View rendering library] to support this use case. + [[webflux-viewresolution-handling]] === Handling @@ -202,13 +204,13 @@ the list of configured `ViewResolver` implementations. trailing slash, and resolve it to a `View`. The same also happens when a view name was not provided (for example, model attribute was returned) or an async return value (for example, `Mono` completed empty). -* {api-spring-framework}/web/reactive/result/view/Rendering.html[Rendering]: API for +* {spring-framework-api}/web/reactive/result/view/Rendering.html[Rendering]: API for view resolution scenarios. Explore the options in your IDE with code completion. * `Model`, `Map`: Extra model attributes to be added to the model for the request. * Any other: Any other return value (except for simple types, as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) +{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) is treated as a model attribute to be added to the model. The attribute name is derived -from the class name by using {api-spring-framework}/core/Conventions.html[conventions], +from the class name by using {spring-framework-api}/core/Conventions.html[conventions], unless a handler method `@ModelAttribute` annotation is present. The model can contain asynchronous, reactive types (for example, from Reactor or RxJava). Prior @@ -238,6 +240,9 @@ operate in terms of logical view names. A view name such as `redirect:/some/resource` is relative to the current application, while a view name such as `redirect:https://example.com/arbitrary/path` redirects to an absolute URL. +NOTE: xref:web/webmvc/mvc-servlet/viewresolver.adoc#mvc-redirecting-forward-prefix[Unlike the Servlet stack], +Spring WebFlux does not support "FORWARD" dispatches, so `forward:` prefixes are not supported as a result. + [[webflux-multiple-representations]] === Content Negotiation diff --git a/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc b/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc index 39f080c10178..1b5a9e643a7e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc @@ -6,4 +6,4 @@ HTTP/2 is supported with Reactor Netty, Tomcat, Jetty, and Undertow. However, there are considerations related to server configuration. For more details, see the -https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support[HTTP/2 wiki page]. +{spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page]. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc b/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc index e9b72f23173e..9b7022fb72a2 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc @@ -38,13 +38,13 @@ code, it becomes important to control the rate of events so that a fast producer overwhelm its destination. Reactive Streams is a -https://github.com/reactive-streams/reactive-streams-jvm/blob/master/README.md#specification[small spec] -(also https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html[adopted] in Java 9) +{reactive-streams-spec}[small spec] +(also {java-api}/java.base/java/util/concurrent/Flow.html[adopted] in Java 9) that defines the interaction between asynchronous components with back pressure. For example a data repository (acting as -https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html[Publisher]) +{reactive-streams-site}/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html[Publisher]) can produce data that an HTTP server (acting as -https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html[Subscriber]) +{reactive-streams-site}/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html[Subscriber]) can then write to the response. The main purpose of Reactive Streams is to let the subscriber control how quickly or how slowly the publisher produces data. @@ -63,10 +63,10 @@ low-level. Applications need a higher-level and richer, functional API to compose async logic -- similar to the Java 8 `Stream` API but not only for collections. This is the role that reactive libraries play. -https://github.com/reactor/reactor[Reactor] is the reactive library of choice for +{reactor-github-org}/reactor[Reactor] is the reactive library of choice for Spring WebFlux. It provides the -https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html[`Mono`] and -https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html[`Flux`] API types +{reactor-site}/docs/core/release/api/reactor/core/publisher/Mono.html[`Mono`] and +{reactor-site}/docs/core/release/api/reactor/core/publisher/Flux.html[`Flux`] API types to work on data sequences of 0..1 (`Mono`) and 0..N (`Flux`) through a rich set of operators aligned with the ReactiveX https://reactivex.io/documentation/operators.html[vocabulary of operators]. Reactor is a Reactive Streams library and, therefore, all of its operators support non-blocking back pressure. @@ -101,7 +101,7 @@ On that foundation, Spring WebFlux provides a choice of two programming models: from the `spring-web` module. Both Spring MVC and WebFlux controllers support reactive (Reactor and RxJava) return types, and, as a result, it is not easy to tell them apart. One notable difference is that WebFlux also supports reactive `@RequestBody` arguments. -* <>: Lambda-based, lightweight, and functional programming model. You can think of +* xref:web/webflux-functional.adoc[Functional Endpoints]: Lambda-based, lightweight, and functional programming model. You can think of this as a small library or a set of utilities that an application can use to route and handle requests. The big difference with annotated controllers is that the application is in charge of request handling from start to finish versus declaring intent through diff --git a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc index 4be5c9b4311c..6e980e5197e4 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc @@ -13,7 +13,7 @@ request handling, on top of which concrete programming models such as annotated controllers and functional endpoints are built. * For the client side, there is a basic `ClientHttpConnector` contract to perform HTTP requests with non-blocking I/O and Reactive Streams back pressure, along with adapters for -https://github.com/reactor/reactor-netty[Reactor Netty], reactive +{reactor-github-org}/reactor-netty[Reactor Netty], reactive https://github.com/jetty-project/jetty-reactive-httpclient[Jetty HttpClient] and https://hc.apache.org/[Apache HttpComponents]. The higher level xref:web/webflux-webclient.adoc[WebClient] used in applications @@ -26,7 +26,7 @@ deserialization of HTTP request and response content. [[webflux-httphandler]] == `HttpHandler` -{api-spring-framework}/http/server/reactive/HttpHandler.html[HttpHandler] +{spring-framework-api}/http/server/reactive/HttpHandler.html[HttpHandler] is a simple contract with a single method to handle a request and a response. It is intentionally minimal, and its main and only purpose is to be a minimal abstraction over different HTTP server APIs. @@ -39,7 +39,7 @@ The following table describes the supported server APIs: | Netty | Netty API -| https://github.com/reactor/reactor-netty[Reactor Netty] +| {reactor-github-org}/reactor-netty[Reactor Netty] | Undertow | Undertow API @@ -59,7 +59,7 @@ The following table describes the supported server APIs: |=== The following table describes server dependencies (also see -https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-the-Spring-Framework[supported versions]): +{spring-framework-wiki}/What%27s-New-in-the-Spring-Framework[supported versions]): |=== |Server name|Group id|Artifact name @@ -88,7 +88,7 @@ The code snippets below show using the `HttpHandler` adapters with each server A ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpHandler handler = ... ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); @@ -97,7 +97,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val handler: HttpHandler = ... val adapter = ReactorHttpHandlerAdapter(handler) @@ -110,7 +110,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpHandler handler = ... UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler); @@ -120,7 +120,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val handler: HttpHandler = ... val adapter = UndertowHttpHandlerAdapter(handler) @@ -134,7 +134,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpHandler handler = ... Servlet servlet = new TomcatHttpHandlerAdapter(handler); @@ -151,7 +151,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val handler: HttpHandler = ... val servlet = TomcatHttpHandlerAdapter(handler) @@ -173,7 +173,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpHandler handler = ... Servlet servlet = new JettyHttpHandlerAdapter(handler); @@ -192,7 +192,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val handler: HttpHandler = ... val servlet = JettyHttpHandlerAdapter(handler) @@ -213,7 +213,7 @@ Kotlin:: *Servlet Container* To deploy as a WAR to any Servlet container, you can extend and include -{api-spring-framework}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`] +{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`. @@ -224,9 +224,9 @@ that as a `Servlet`. The `org.springframework.web.server` package builds on the xref:web/webflux/reactive-spring.adoc#webflux-httphandler[`HttpHandler`] contract to provide a general-purpose web API for processing requests through a chain of multiple -{api-spring-framework}/web/server/WebExceptionHandler.html[`WebExceptionHandler`], multiple -{api-spring-framework}/web/server/WebFilter.html[`WebFilter`], and a single -{api-spring-framework}/web/server/WebHandler.html[`WebHandler`] component. The chain can +{spring-framework-api}/web/server/WebExceptionHandler.html[`WebExceptionHandler`], multiple +{spring-framework-api}/web/server/WebFilter.html[`WebFilter`], and a single +{spring-framework-api}/web/server/WebHandler.html[`WebHandler`] component. The chain can be put together with `WebHttpHandlerBuilder` by simply pointing to a Spring `ApplicationContext` where components are xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[auto-detected], and/or by registering components @@ -305,14 +305,14 @@ Spring ApplicationContext, or that can be registered directly with it: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono> getFormData(); ---- Kotlin:: + -[source,Kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,Kotlin,indent=0,subs="verbatim,quotes"] ---- suspend fun getFormData(): MultiValueMap ---- @@ -334,14 +334,14 @@ The `DefaultServerWebExchange` uses the configured `HttpMessageReader` to parse ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Mono> getMultipartData(); ---- Kotlin:: + -[source,Kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,Kotlin,indent=0,subs="verbatim,quotes"] ---- suspend fun getMultipartData(): MultiValueMap ---- @@ -419,6 +419,24 @@ controllers. However, when you use it with Spring Security, we advise relying on See the section on xref:web/webflux-cors.adoc[CORS] and the xref:web/webflux-cors.adoc#webflux-cors-webfilter[CORS `WebFilter`] for more details. +[[filters.url-handler]] +=== URL Handler +[.small]#xref:web/webmvc/filters.adoc#filters.url-handler[See equivalent in the Servlet stack]# + +You may want your controller endpoints to match routes with or without a trailing slash in the URL path. +For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`. + +Adding trailing slash variants to all mapping declarations is not the best way to handle this use case. +The `UrlHandlerFilter` web 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. +* mutate the request to act as if the request was sent without a trailing slash and continue the processing of the request. + +Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application: + +include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0] + + [[webflux-exception-handler]] == Exceptions @@ -438,7 +456,7 @@ The following table describes the available `WebExceptionHandler` implementation | `ResponseStatusExceptionHandler` | Provides handling for exceptions of type - {api-spring-framework}/web/server/ResponseStatusException.html[`ResponseStatusException`] + {spring-framework-api}/web/server/ResponseStatusException.html[`ResponseStatusException`] by setting the response to the HTTP status code of the exception. | `WebFluxResponseStatusExceptionHandler` @@ -453,22 +471,22 @@ The following table describes the available `WebExceptionHandler` implementation [[webflux-codecs]] == Codecs -[.small]#xref:integration/rest-clients.adoc#rest-message-conversion[See equivalent in the Servlet stack]# +[.small]#xref:web/webmvc/message-converters.adoc#message-converters[See equivalent in the Servlet stack]# The `spring-web` and `spring-core` modules provide support for serializing and deserializing byte content to and from higher level objects through non-blocking I/O with Reactive Streams back pressure. The following describes this support: -* {api-spring-framework}/core/codec/Encoder.html[`Encoder`] and -{api-spring-framework}/core/codec/Decoder.html[`Decoder`] are low level contracts to +* {spring-framework-api}/core/codec/Encoder.html[`Encoder`] and +{spring-framework-api}/core/codec/Decoder.html[`Decoder`] are low level contracts to encode and decode content independent of HTTP. -* {api-spring-framework}/http/codec/HttpMessageReader.html[`HttpMessageReader`] and -{api-spring-framework}/http/codec/HttpMessageWriter.html[`HttpMessageWriter`] are contracts +* {spring-framework-api}/http/codec/HttpMessageReader.html[`HttpMessageReader`] and +{spring-framework-api}/http/codec/HttpMessageWriter.html[`HttpMessageWriter`] are contracts to encode and decode HTTP message content. * An `Encoder` can be wrapped with `EncoderHttpMessageWriter` to adapt it for use in a web application, while a `Decoder` can be wrapped with `DecoderHttpMessageReader`. -* {api-spring-framework}/core/io/buffer/DataBuffer.html[`DataBuffer`] abstracts different -byte buffer representations (e.g. Netty `ByteBuf`, `java.nio.ByteBuffer`, etc.) and is +* {spring-framework-api}/core/io/buffer/DataBuffer.html[`DataBuffer`] abstracts different +byte buffer representations (for example, Netty `ByteBuf`, `java.nio.ByteBuffer`, etc.) and is what all codecs work on. See xref:core/databuffer-codec.adoc[Data Buffers and Codecs] in the "Spring Core" section for more on this topic. @@ -485,7 +503,7 @@ xref:web/webflux/config.adoc#webflux-config-message-codecs[HTTP message codecs]. [[webflux-codecs-jackson]] === Jackson JSON -JSON and binary JSON (https://github.com/FasterXML/smile-format-specification[Smile]) are +JSON and binary JSON ({jackson-github-org}/smile-format-specification[Smile]) are both supported when the Jackson library is present. The `Jackson2Decoder` works as follows: @@ -493,8 +511,8 @@ The `Jackson2Decoder` works as follows: * Jackson's asynchronous, non-blocking parser is used to aggregate a stream of byte chunks into ``TokenBuffer``'s each representing a JSON object. * Each `TokenBuffer` is passed to Jackson's `ObjectMapper` to create a higher level object. -* When decoding to a single-value publisher (e.g. `Mono`), there is one `TokenBuffer`. -* When decoding to a multi-value publisher (e.g. `Flux`), each `TokenBuffer` is passed to +* When decoding to a single-value publisher (for example, `Mono`), there is one `TokenBuffer`. +* When decoding to a multi-value publisher (for example, `Flux`), each `TokenBuffer` is passed to the `ObjectMapper` as soon as enough bytes are received for a fully formed object. The input content can be a JSON array, or any https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format such as NDJSON, @@ -502,7 +520,7 @@ JSON Lines, or JSON Text Sequences. The `Jackson2Encoder` works as follows: -* For a single value publisher (e.g. `Mono`), simply serialize it through the +* For a single value publisher (for example, `Mono`), simply serialize it through the `ObjectMapper`. * For a multi-value publisher with `application/json`, by default collect the values with `Flux#collectToList()` and then serialize the resulting collection. @@ -549,7 +567,7 @@ for the actual parsing to a `Flux` and then simply collects the parts into By default, the `DefaultPartHttpMessageReader` is used, but this can be changed through the `ServerCodecConfigurer`. For more information about the `DefaultPartHttpMessageReader`, refer to the -{api-spring-framework}/http/codec/multipart/DefaultPartHttpMessageReader.html[javadoc of `DefaultPartHttpMessageReader`]. +{spring-framework-api}/http/codec/multipart/DefaultPartHttpMessageReader.html[javadoc of `DefaultPartHttpMessageReader`]. On the server side where multipart form content may need to be accessed from multiple places, `ServerWebExchange` provides a dedicated `getMultipartData()` method that parses @@ -562,6 +580,18 @@ for repeated, map-like access to parts, or otherwise rely on the `SynchronossPartHttpMessageReader` for a one-time access to `Flux`. +[[webflux-codecs-protobuf]] +=== Protocol Buffers + +`ProtobufEncoder` and `ProtobufDecoder` supporting decoding and encoding "application/x-protobuf", "application/octet-stream" +and "application/vnd.google.protobuf" content for `com.google.protobuf.Message` types. They also support stream of values +if content is received/sent with the "delimited" parameter along the content type (like "application/x-protobuf;delimited=true"). +This requires the "com.google.protobuf:protobuf-java" library, version 3.29 and higher. + +The `ProtobufJsonDecoder` and `ProtobufJsonEncoder` variants support reading and writing JSON documents to and from Protobuf messages. +They require the "com.google.protobuf:protobuf-java-util" dependency. Note, the JSON variants do not support reading stream of messages, +see the {spring-framework-api}/http/codec/protobuf/ProtobufJsonDecoder.html[javadoc of `ProtobufJsonDecoder`] for more details. + [[webflux-codecs-limits]] === Limits @@ -643,11 +673,11 @@ is not useful for correlating log messages that belong to a specific request. Th WebFlux log messages are prefixed with a request-specific ID by default. On the server side, the log ID is stored in the `ServerWebExchange` attribute -({api-spring-framework}/web/server/ServerWebExchange.html#LOG_ID_ATTRIBUTE[`LOG_ID_ATTRIBUTE`]), +({spring-framework-api}/web/server/ServerWebExchange.html#LOG_ID_ATTRIBUTE[`LOG_ID_ATTRIBUTE`]), while a fully formatted prefix based on that ID is available from `ServerWebExchange#getLogPrefix()`. On the `WebClient` side, the log ID is stored in the `ClientRequest` attribute -({api-spring-framework}/web/reactive/function/client/ClientRequest.html#LOG_ID_ATTRIBUTE[`LOG_ID_ATTRIBUTE`]) +({spring-framework-api}/web/reactive/function/client/ClientRequest.html#LOG_ID_ATTRIBUTE[`LOG_ID_ATTRIBUTE`]) ,while a fully formatted prefix is available from `ClientRequest#logPrefix()`. @@ -664,7 +694,7 @@ The following example shows how to do so for server-side requests: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -679,7 +709,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -698,7 +728,7 @@ The following example shows how to do so for client-side requests: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Consumer consumer = configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true); @@ -710,7 +740,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) } @@ -748,7 +778,7 @@ The following example shows how to do so for client-side requests: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- WebClient webClient = WebClient.builder() .codecs(configurer -> { @@ -760,7 +790,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val webClient = WebClient.builder() .codecs({ configurer -> diff --git a/framework-docs/modules/ROOT/pages/web/webflux/security.adoc b/framework-docs/modules/ROOT/pages/web/webflux/security.adoc index 4c448e3cba7b..fcb982d254cb 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/security.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/security.adoc @@ -4,7 +4,7 @@ [.small]#xref:web/webmvc/mvc-security.adoc[See equivalent in the Servlet stack]# -The https://spring.io/projects/spring-security[Spring Security] project provides support +The {spring-site-projects}/spring-security[Spring Security] project provides support for protecting web applications from malicious exploits. See the Spring Security reference documentation, including: diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc index 3a8596d8250a..032311bff3dc 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc @@ -1,5 +1,6 @@ [[mvc-cors]] = CORS + [.small]#xref:web/webflux-cors.adoc[See equivalent in the Reactive stack]# Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section @@ -76,7 +77,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r and, consequently, browsers reject them. Each `HandlerMapping` can be -{api-spring-framework}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured] +{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured] individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications use the MVC Java configuration or the XML namespace to declare such mappings, which results in a single global map being passed to all `HandlerMapping` instances. @@ -88,8 +89,8 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement The rules for combining global and local configuration are generally additive -- for example, all global and all local origins. For those attributes where only a single value can be -accepted, e.g. `allowCredentials` and `maxAge`, the local overrides the global value. See -{api-spring-framework}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`] +accepted, for example, `allowCredentials` and `maxAge`, the local overrides the global value. See +{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`] for more details. [TIP] @@ -108,7 +109,7 @@ To learn more from the source or make advanced customizations, check the code be == `@CrossOrigin` [.small]#xref:web/webflux-cors.adoc#webflux-cors-controller[See equivalent in the Reactive stack]# -The {api-spring-framework}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] +The {spring-framework-api}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] annotation enables cross-origin requests on annotated controller methods, as the following example shows: @@ -116,7 +117,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/account") @@ -137,7 +138,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/account") @@ -178,7 +179,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin(origins = "https://domain2.com", maxAge = 3600) @RestController @@ -199,7 +200,7 @@ public class AccountController { Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin(origins = ["https://domain2.com"], maxAge = 3600) @RestController @@ -225,7 +226,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin(maxAge = 3600) @RestController @@ -247,7 +248,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin(maxAge = 3600) @RestController @@ -308,7 +309,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc @@ -331,7 +332,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc @@ -385,7 +386,7 @@ as the following example shows: [.small]#xref:web/webflux-cors.adoc#webflux-cors-webfilter[See equivalent in the Reactive stack]# You can apply CORS support through the built-in -{api-spring-framework}/web/filter/CorsFilter.html[`CorsFilter`]. +{spring-framework-api}/web/filter/CorsFilter.html[`CorsFilter`]. NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for @@ -398,7 +399,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- CorsConfiguration config = new CorsConfiguration(); @@ -418,7 +419,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- val config = CorsConfiguration() diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc index 8d146310be73..354075203a92 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc @@ -1,6 +1,7 @@ [[webmvc-fn]] = Functional Endpoints -[.small]#<># + +[.small]#xref:web/webflux-functional.adoc[See equivalent in the Reactive stack]# Spring Web MVC includes WebMvc.fn, a lightweight functional programming model in which functions are used to route and handle requests and contracts are designed for immutability. @@ -34,7 +35,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.servlet.function.RequestPredicates.*; @@ -71,7 +72,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.servlet.function.router @@ -134,14 +135,14 @@ The following example extracts the request body to a `String`: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- String string = request.body(String.class); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val string = request.body() ---- @@ -155,14 +156,14 @@ where `Person` objects are decoded from a serialized form, such as JSON or XML: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- List people = request.body(new ParameterizedTypeReference>() {}); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val people = request.body() ---- @@ -174,14 +175,14 @@ The following example shows how to access parameters: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- MultiValueMap params = request.params(); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val map = request.params() ---- @@ -200,7 +201,7 @@ content: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Person person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person); @@ -208,7 +209,7 @@ ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person); Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val person: Person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person) @@ -221,7 +222,7 @@ The following example shows how to build a 201 (CREATED) response with a `Locati ====== Java:: + -[source,java,role="primary"] +[source,java] ---- URI location = ... ServerResponse.created(location).build(); @@ -229,7 +230,7 @@ ServerResponse.created(location).build(); Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val location: URI = ... ServerResponse.created(location).build() @@ -243,7 +244,7 @@ You can also use an asynchronous result as the body, in the form of a `Completab ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Mono person = webClient.get().retrieve().bodyToMono(Person.class); ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person); @@ -251,7 +252,7 @@ ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person); Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val person = webClient.get().retrieve().awaitBody() ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person) @@ -267,7 +268,7 @@ any other asynchronous type supported by the `ReactiveAdapterRegistry`. For inst ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Mono asyncResponse = webClient.get().retrieve().bodyToMono(Person.class) .map(p -> ServerResponse.ok().header("Name", p.name()).body(p)); @@ -275,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: @@ -283,7 +284,7 @@ allows you to send Strings, or other objects as JSON. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public RouterFunction sse() { return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> { @@ -309,7 +310,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun sse(): RouterFunction = router { GET("/sse") { request -> ServerResponse.sse { sseBuilder -> @@ -346,7 +347,7 @@ We can write a handler function as a lambda, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HandlerFunction helloWorld = request -> ServerResponse.ok().body("Hello World"); @@ -354,7 +355,7 @@ HandlerFunction helloWorld = Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val helloWorld: (ServerRequest) -> ServerResponse = { ServerResponse.ok().body("Hello World") } @@ -373,7 +374,7 @@ For example, the following class exposes a reactive `Person` repository: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.reactive.function.server.ServerResponse.ok; @@ -419,7 +420,7 @@ found. If it is not found, we return a 404 Not Found response. Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class PersonHandler(private val repository: PersonRepository) { @@ -463,7 +464,7 @@ xref:web/webmvc/mvc-config/validation.adoc[Validator] implementation for a `Pers ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class PersonHandler { @@ -493,7 +494,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class PersonHandler(private val repository: PersonRepository) { @@ -563,7 +564,7 @@ header: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RouterFunction route = RouterFunctions.route() .GET("/hello-world", accept(MediaType.TEXT_PLAIN), @@ -572,7 +573,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.servlet.function.router @@ -624,7 +625,7 @@ The following example shows the composition of four routes: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.servlet.function.RequestPredicates.*; @@ -651,7 +652,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.web.servlet.function.router @@ -693,7 +694,7 @@ For instance, the last few lines of the example above can be improved in the fol ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RouterFunction route = route() .path("/person", builder -> builder // <1> @@ -706,7 +707,7 @@ RouterFunction route = route() Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.servlet.function.router @@ -730,7 +731,7 @@ We can further improve by using the `nest` method together with `accept`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RouterFunction route = route() .path("/person", b1 -> b1 @@ -743,7 +744,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.servlet.function.router @@ -760,6 +761,71 @@ Kotlin:: ====== +[[webmvc-fn-serving-resources]] +== Serving Resources + +WebMvc.fn provides built-in support for serving resources. + +NOTE: In addition to the capabilities described below, it is possible to implement even more flexible resource handling thanks to +{spring-framework-api}++/web/servlet/function/RouterFunctions.html#resources(java.util.function.Function)++[`RouterFunctions#resource(java.util.function.Function)`]. + +[[webmvc-fn-resource]] +=== Redirecting to a resource + +It is possible to redirect requests matching a specified predicate to a resource. This can be useful, for example, +for handling redirects in Single Page Applications. + +[tabs] +====== +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")).negate(); + RouterFunction redirectToIndex = route() + .resource(spaPredicate, index) + .build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val redirectToIndex = router { + val index = ClassPathResource("static/index.html") + val spaPredicate = !(path("/api/**") or path("/error")) + resource(spaPredicate, index) + } +---- +====== + +[[webmvc-fn-resources]] +=== Serving resources from a root location + +It is also possible to route requests that match a given pattern to resources relative to a given root location. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + 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) } +---- +====== + + [[webmvc-fn-running]] == Running a Server [.small]#xref:web/webflux-functional.adoc#webflux-fn-running[See equivalent in the Reactive stack]# @@ -786,7 +852,7 @@ The following example shows a WebFlux Java configuration: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableMvc @@ -823,7 +889,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableMvc @@ -874,7 +940,7 @@ For instance, consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RouterFunction route = route() .path("/person", b1 -> b1 @@ -893,7 +959,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.servlet.function.router @@ -931,7 +997,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- SecurityManager securityManager = ... @@ -954,7 +1020,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.servlet.function.router diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-test.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-test.adoc index e5633bcea279..8456bdf01538 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-test.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-test.adoc @@ -16,7 +16,7 @@ See xref:testing/testcontext-framework.adoc[TestContext Framework] for more deta * Spring MVC Test: A framework, also known as `MockMvc`, for testing annotated controllers through the `DispatcherServlet` (that is, supporting annotations), complete with the Spring MVC infrastructure but without an HTTP server. -See xref:testing/spring-mvc-test-framework.adoc[Spring MVC Test] for more details. +See xref:testing/mockmvc.adoc[Spring MVC Test] for more details. * Client-side REST: `spring-test` provides a `MockRestServiceServer` that you can use as a mock server for testing client-side code that internally uses the `RestTemplate`. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view.adoc index e6af04b7fe26..a00afbd84cdd 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view.adoc @@ -1,12 +1,14 @@ [[mvc-view]] = View Technologies :page-section-summary-toc: 1 + [.small]#xref:web/webflux-view.adoc[See equivalent in the Reactive stack]# -The use of view technologies in Spring MVC is pluggable. Whether you decide to use +The rendering of views in Spring MVC is pluggable. Whether you decide to use Thymeleaf, Groovy Markup Templates, JSPs, or other technologies is primarily a matter of a configuration change. This chapter covers view technologies integrated with Spring MVC. -We assume you are already familiar with xref:web/webmvc/mvc-servlet/viewresolver.adoc[View Resolution]. + +For more context on view rendering, please see xref:web/webmvc/mvc-servlet/viewresolver.adoc[View Resolution]. WARNING: The views of a Spring MVC application live within the internal trust boundaries of that application. Views have access to all the beans of your application context. As diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-document.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-document.adoc index 64a49310d125..469e7e5bd471 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-document.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-document.adoc @@ -36,7 +36,7 @@ A simple PDF view for a word list could extend ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class PdfWordList extends AbstractPdfView { @@ -53,7 +53,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class PdfWordList : AbstractPdfView() { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc index 68714b3dfe5c..02f1512afbf8 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc @@ -14,7 +14,7 @@ empty). The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SampleContentAtomView extends AbstractAtomFeedView { @@ -34,7 +34,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SampleContentAtomView : AbstractAtomFeedView() { @@ -57,7 +57,7 @@ Similar requirements apply for implementing `AbstractRssFeedView`, as the follow ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SampleContentRssView extends AbstractRssFeedView { @@ -77,7 +77,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SampleContentRssView : AbstractRssFeedView() { @@ -102,7 +102,7 @@ cookies or other HTTP headers. The feed is automatically written to the response object after the method returns. For an example of creating an Atom view, see Alef Arendsen's Spring Team Blog -https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-spring-s-rest-support[entry]. +{spring-site-blog}/2009/03/16/adding-an-atom-view-to-an-application-using-spring-s-rest-support[entry]. 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 new file mode 100644 index 000000000000..953883659a9c --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-fragments.adoc @@ -0,0 +1,118 @@ +[[mvc-view-fragments]] += HTML Fragments +:page-section-summary-toc: 1 + +[.small]#xref:web/webflux-view.adoc#webflux-view-fragments[See equivalent in the Reactive stack]# + +https://htmx.org/[HTMX] and https://turbo.hotwired.dev/[Hotwire Turbo] emphasize an +HTML-over-the-wire approach where clients receive server updates in HTML rather than in JSON. +This allows the benefits of an SPA (single page app) without having to write much or even +any JavaScript. For a good overview and to learn more, please visit their respective +websites. + +In Spring MVC, view rendering typically involves specifying one view and one model. +However, in HTML-over-the-wire a common capability is to send multiple HTML fragments that +the browser can use to update different parts of the page. For this, controller methods +can return `Collection`. For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + List handle() { + return List.of(new ModelAndView("posts"), new ModelAndView("comments")); + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + fun handle(): List { + return listOf(ModelAndView("posts"), ModelAndView("comments")) + } +---- +====== + +The same can be done also by returning the dedicated type `FragmentsRendering`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + FragmentsRendering handle() { + return FragmentsRendering.fragment("posts").fragment("comments").build(); + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + fun handle(): FragmentsRendering { + return FragmentsRendering.fragment("posts").fragment("comments").build() + } +---- +====== + +Each fragment can have an independent model, and that model inherits attributes from the +shared model for the request. + +HTMX and Hotwire Turbo support streaming updates over SSE (server-sent events). +A controller can use `SseEmitter` to send `ModelAndView` to render a fragment per event: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + SseEmitter handle() { + SseEmitter emitter = new SseEmitter(); + startWorkerThread(() -> { + try { + emitter.send(SseEmitter.event().data(new ModelAndView("posts"))); + emitter.send(SseEmitter.event().data(new ModelAndView("comments"))); + // ... + } + catch (IOException ex) { + // Cancel sending + } + }); + return emitter; + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @GetMapping + fun handle(): SseEmitter { + val emitter = SseEmitter() + startWorkerThread{ + try { + emitter.send(SseEmitter.event().data(ModelAndView("posts"))) + emitter.send(SseEmitter.event().data(ModelAndView("comments"))) + // ... + } + catch (ex: IOException) { + // Cancel sending + } + } + return emitter + } +---- +====== + +The same can also be done by returning `Flux`, or any other type adaptable +to a Reactive Streams `Publisher` through the `ReactiveAdapterRegistry`. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-freemarker.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-freemarker.adoc index e0e95083b980..0fadef3d1e23 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-freemarker.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-freemarker.adoc @@ -19,7 +19,7 @@ The following example shows how to configure FreeMarker as a view technology: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc @@ -36,6 +36,7 @@ Java:: public FreeMarkerConfigurer freeMarkerConfigurer() { FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); configurer.setTemplateLoaderPath("/WEB-INF/freemarker"); + configurer.setDefaultCharset(StandardCharsets.UTF_8); return configurer; } } @@ -43,7 +44,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc @@ -58,6 +59,7 @@ Kotlin:: @Bean fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { setTemplateLoaderPath("/WEB-INF/freemarker") + setDefaultCharset(StandardCharsets.UTF_8) } } ---- @@ -86,6 +88,7 @@ properties, as the following example shows: ---- + ---- @@ -127,6 +130,7 @@ the `Configuration` object. [[mvc-view-freemarker-forms]] == Form Handling +[.small]#xref:web/webflux-view.adoc#webflux-view-freemarker-forms[See equivalent in the Reactive stack]# Spring provides a tag library for use in JSPs that contains, among others, a `` element. This element primarily lets forms display values from @@ -374,7 +378,7 @@ codes with suitable keys, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- protected Map referenceData(HttpServletRequest request) throws Exception { Map cityMap = new LinkedHashMap<>(); @@ -390,7 +394,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- protected fun referenceData(request: HttpServletRequest): Map { val cityMap = linkedMapOf( diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-groovymarkup.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-groovymarkup.adoc index 7793e08c1048..5df5c7a93d6e 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-groovymarkup.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-groovymarkup.adoc @@ -19,7 +19,7 @@ The following example shows how to configure the Groovy Markup Template Engine: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc @@ -43,7 +43,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jackson.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jackson.adoc index fd8f53404349..3b419811c325 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jackson.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jackson.adoc @@ -31,12 +31,12 @@ serializers and deserializers for specific types. [.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]# `MappingJackson2XmlView` uses the -https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML extension's] `XmlMapper` +{jackson-github-org}/jackson-dataformat-xml[Jackson XML extension's] `XmlMapper` to render the response content as XML. If the model contains multiple entries, you should explicitly set the object to be serialized by using the `modelKey` bean property. If the model contains a single entry, it is serialized automatically. -You can customized XML mapping as needed by using JAXB or Jackson's provided +You can customize XML mapping as needed by using JAXB or Jackson's provided annotations. When you need further control, you can inject a custom `XmlMapper` through the `ObjectMapper` property, for cases where custom XML you need to provide serializers and deserializers for specific types. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc index 1e37619d3932..8d095e720d5b 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc @@ -44,7 +44,7 @@ Spring tags have HTML escaping features to enable or disable escaping of charact The `spring.tld` tag library descriptor (TLD) is included in the `spring-webmvc.jar`. For a comprehensive reference on individual tags, browse the -{api-spring-framework}/web/servlet/tags/package-summary.html#package.description[API reference] +{spring-framework-api}/web/servlet/tags/package-summary.html#package.description[API reference] or see the tag library description. @@ -189,7 +189,7 @@ hobbies. The following example shows the `Preferences` class: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class Preferences { @@ -225,7 +225,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Preferences( var receiveNewsletter: Boolean, @@ -592,7 +592,7 @@ called `UserValidator`, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class UserValidator implements Validator { @@ -609,7 +609,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class UserValidator : Validator { @@ -683,7 +683,7 @@ the HTML would be as follows: ---- What if we want to display the entire list of errors for a given page? The next example -shows that the `errors` tag also supports some basic wildcarding functionality. +shows that the `errors` tag also supports some basic wildcard functionality. * `path="{asterisk}"`: Displays all errors. * `path="lastName"`: Displays all errors associated with the `lastName` field. @@ -745,7 +745,7 @@ The HTML would be as follows: The `spring-form.tld` tag library descriptor (TLD) is included in the `spring-webmvc.jar`. For a comprehensive reference on individual tags, browse the -{api-spring-framework}/web/servlet/tags/form/package-summary.html#package.description[API reference] +{spring-framework-api}/web/servlet/tags/form/package-summary.html#package.description[API reference] or see the tag library description. @@ -801,7 +801,7 @@ The following example shows the corresponding `@Controller` method: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RequestMapping(method = RequestMethod.DELETE) public String deletePet(@PathVariable int ownerId, @PathVariable int petId) { @@ -812,7 +812,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RequestMapping(method = [RequestMethod.DELETE]) fun deletePet(@PathVariable ownerId: Int, @PathVariable petId: Int): String { 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 27fac970a199..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 @@ -5,7 +5,7 @@ The Spring Framework has a built-in integration for using Spring MVC with any templating library that can run on top of the -https://www.jcp.org/en/jsr/detail?id=223[JSR-223] Java scripting engine. We have tested the following +{JSR}223[JSR-223] Java scripting engine. We have tested the following templating libraries on different script engines: [%header] @@ -13,11 +13,11 @@ 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] |https://kotlinlang.org/[Kotlin] +|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin] |=== TIP: The basic rule for integrating any other script engine is that it must implement the @@ -47,7 +47,7 @@ through https://www.webjars.org/[WebJars]. [[mvc-view-script-integrate]] == Script Templates -[.small]#xref:web/webflux-view.adoc#webflux-view-script[See equivalent in the Reactive stack]# +[.small]#xref:web/webflux-view.adoc#webflux-view-script-integrate[See equivalent in the Reactive stack]# You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use, the script files to load, what function to call to render templates, and so on. @@ -57,7 +57,7 @@ The following example uses Mustache templates and the Nashorn JavaScript engine: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc @@ -82,7 +82,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc @@ -124,7 +124,7 @@ The controller would look no different for the Java and XML configurations, as t ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class SampleController { @@ -140,7 +140,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class SampleController { @@ -174,7 +174,7 @@ The render function is called with the following parameters: * `String template`: The template content * `Map model`: The view model * `RenderingContext renderingContext`: The - {api-spring-framework}/web/servlet/view/script/RenderingContext.html[`RenderingContext`] + {spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`] that gives access to the application context, the locale, the template loader, and the URL (since 5.0) @@ -192,7 +192,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc @@ -217,7 +217,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebMvc @@ -265,8 +265,8 @@ template engine configuration, for example). The following example shows how to ---- Check out the Spring Framework unit tests, -{spring-framework-main-code}/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[Java], and -{spring-framework-main-code}/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources], +{spring-framework-code}/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[Java], and +{spring-framework-code}/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources], for more configuration examples. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-xslt.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-xslt.adoc index 255c899eb6e2..7fe25e7164a6 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-xslt.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-xslt.adoc @@ -26,7 +26,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @EnableWebMvc @ComponentScan @@ -45,7 +45,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @EnableWebMvc @ComponentScan @@ -74,7 +74,7 @@ handler method being defined as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class XsltController { @@ -100,7 +100,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.ui.set diff --git a/framework-docs/modules/ROOT/pages/web/webmvc.adoc b/framework-docs/modules/ROOT/pages/web/webmvc.adoc index d75c3c842500..8eaac910662d 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc.adoc @@ -7,19 +7,16 @@ Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, "Spring Web MVC," comes from the name of its source module -({spring-framework-main-code}/spring-webmvc[`spring-webmvc`]), +({spring-framework-code}/spring-webmvc[`spring-webmvc`]), but it is more commonly known as "Spring MVC". Parallel to Spring Web MVC, Spring Framework 5.0 introduced a reactive-stack web framework whose name, "Spring WebFlux," is also based on its source module -({spring-framework-main-code}/spring-webflux[`spring-webflux`]). -This chapter covers Spring Web MVC. The xref:testing/unit.adoc#mock-objects-web-reactive[next chapter] -covers Spring WebFlux. +({spring-framework-code}/spring-webflux[`spring-webflux`]). +This chapter covers Spring Web MVC. For reactive-stack web applications, see +xref:web-reactive.adoc[Web on Reactive Stack]. For baseline information and compatibility with Servlet container and Jakarta EE version ranges, see the Spring Framework -https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions[Wiki]. - - - +{spring-framework-wiki}/Spring-Framework-Versions[Wiki]. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc index 27629d92a77b..9bd13191aec1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc @@ -9,7 +9,11 @@ The `spring-web` module provides some useful filters: * xref:web/webmvc/filters.adoc#filters-forwarded-headers[Forwarded Headers] * xref:web/webmvc/filters.adoc#filters-shallow-etag[Shallow ETag] * xref:web/webmvc/filters.adoc#filters-cors[CORS] +* xref:web/webmvc/filters.adoc#filters.url-handler[URL Handler] +Servlet filters can be configured in the `web.xml` configuration file or using Servlet annotations. +If you are using Spring Boot, you can +{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare them as beans and configure them as part of your application]. [[filters-http-put]] @@ -26,7 +30,7 @@ available through the `ServletRequest.getParameter{asterisk}()` family of method -[[forwarded-headers]] +[[filters-forwarded-headers]] == Forwarded Headers [.small]#xref:web/webflux/reactive-spring.adoc#webflux-forwarded-headers[See equivalent in the Reactive stack]# @@ -78,13 +82,14 @@ it does the same, but it also compares the computed value against the `If-None-M request header and, if the two are equal, returns a 304 (NOT_MODIFIED). This strategy saves network bandwidth but not CPU, as the full response must be computed for each request. -State-changing HTTP methods and other HTTP conditional request headers such as `If-Match` and `If-Unmodified-Since` are outside the scope of this filter. -Other strategies at the controller level can avoid the computation and have a broader support for HTTP conditional requests. +State-changing HTTP methods and other HTTP conditional request headers such as `If-Match` and +`If-Unmodified-Since` are outside the scope of this filter. Other strategies at the controller level +can avoid the computation and have a broader support for HTTP conditional requests. See xref:web/webmvc/mvc-caching.adoc[HTTP Caching]. This filter has a `writeWeakETag` parameter that configures the filter to write weak ETags similar to the following: `W/"02a2d595e6ed9a0b24f027f2b63b134d6"` (as defined in -https://tools.ietf.org/html/rfc7232#section-2.3[RFC 7232 Section 2.3]). +{rfc-site}/rfc7232#section-2.3[RFC 7232 Section 2.3]). In order to support xref:web/webmvc/mvc-ann-async.adoc[asynchronous requests] this filter must be mapped with `DispatcherType.ASYNC` so that the filter can delay and successfully generate an @@ -108,4 +113,22 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors +[[filters.url-handler]] +== URL Handler +[.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 means that sending a "GET /home/" request would be +handled by a controller method annotated with `@GetMapping("/home")`. + +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. + +Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application: + +include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc new file mode 100644 index 000000000000..fc6d5a184761 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc @@ -0,0 +1,88 @@ +[[message-converters]] += HTTP Message Conversion + +[.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs[See equivalent in the Reactive stack]# + +The `spring-web` module contains the `HttpMessageConverter` interface for reading and writing the body of HTTP requests and responses through `InputStream` and `OutputStream`. +`HttpMessageConverter` instances are used on the client side (for example, in the `RestClient`) and on the server side (for example, in Spring MVC REST controllers). + +Concrete implementations for the main media (MIME) types are provided in the framework and are, by default, registered with the `RestClient` and `RestTemplate` on the client side and with `RequestMappingHandlerAdapter` on the server side (see xref:web/webmvc/mvc-config/message-converters.adoc[Configuring Message Converters]). + +Several implementations of `HttpMessageConverter` are described below. +Refer to the {spring-framework-api}/http/converter/HttpMessageConverter.html[`HttpMessageConverter` Javadoc] for the complete list. +For all converters, a default media type is used, but you can override it by setting the `supportedMediaTypes` property. + +[[rest-message-converters-tbl]] +.HttpMessageConverter Implementations +[cols="1,3"] +|=== +| MessageConverter | Description + +| `StringHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write `String` instances from the HTTP request and response. +By default, this converter supports all text media types(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`. + +| `FormHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write form data from the HTTP request and response. +By default, this converter reads and writes the `application/x-www-form-urlencoded` media type. +Form data is read from and written into a `MultiValueMap`. +The converter can also write (but not read) multipart data read from a `MultiValueMap`. +By default, `multipart/form-data` is supported. +Additional multipart subtypes can be supported for writing form data. +Consult the javadoc for `FormHttpMessageConverter` for further details. + +| `ByteArrayHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write byte arrays from the HTTP request and response. +By default, this converter supports all media types (`{asterisk}/{asterisk}`) and writes with a `Content-Type` of `application/octet-stream`. +You can override this by setting the `supportedMediaTypes` property and overriding `getContentType(byte[])`. + +| `MarshallingHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write XML by using Spring's `Marshaller` and `Unmarshaller` abstractions from the `org.springframework.oxm` package. +This converter requires a `Marshaller` and `Unmarshaller` before it can be used. +You can inject these through constructor or bean properties. +By default, this converter supports `text/xml` and `application/xml`. + +| `MappingJackson2HttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `ObjectMapper`. +You can customize JSON mapping as needed through the use of Jackson's provided annotations. +When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `ObjectMapper` through the `ObjectMapper` property. +By default, this converter supports `application/json`. This requires the `com.fasterxml.jackson.core:jackson-databind` dependency. + +| `MappingJackson2XmlHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write XML by using {jackson-github-org}/jackson-dataformat-xml[Jackson XML] extension's `XmlMapper`. +You can customize XML mapping as needed through the use of JAXB or Jackson's provided annotations. +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` + +| `SourceHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write `javax.xml.transform.Source` from the HTTP request and response. +Only `DOMSource`, `SAXSource`, and `StreamSource` are supported. +By default, this converter supports `text/xml` and `application/xml`. + +| `GsonHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write JSON by using "Google Gson". +This requires the `com.google.code.gson:gson` dependency. + +| `JsonbHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write JSON by using the Jakarta Json Bind API. +This requires the `jakarta.json.bind:jakarta.json.bind-api` dependency and an implementation available. + +| `ProtobufHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write Protobuf messages in binary format with the `"application/x-protobuf"` +content type. This requires the `com.google.protobuf:protobuf-java` dependency. + +| `ProtobufJsonFormatHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write JSON documents to and from Protobuf messages. +This requires the `com.google.protobuf:protobuf-java-util` dependency. + +|=== + + 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 8f4c06f25b56..7c67abdfcf7a 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 @@ -25,7 +25,7 @@ return value with `DeferredResult`, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/quotes") @ResponseBody @@ -41,7 +41,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/quotes") @ResponseBody @@ -71,7 +71,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping public Callable processUpload(final MultipartFile file) { @@ -81,7 +81,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping fun processUpload(file: MultipartFile) = Callable { @@ -137,7 +137,7 @@ Here is a very concise overview of Servlet asynchronous request processing: asynchronously produced return value from the `Callable`. For further background and context, you can also read -https://spring.io/blog/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support[the +{spring-site-blog}/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support[the blog posts] that introduced asynchronous request processing support in Spring MVC 3.2. @@ -165,11 +165,11 @@ processing (instead of `postHandle` and `afterCompletion`). `HandlerInterceptor` implementations can also register a `CallableProcessingInterceptor` or a `DeferredResultProcessingInterceptor`, to integrate more deeply with the lifecycle of an asynchronous request (for example, to handle a timeout event). See -{api-spring-framework}/web/servlet/AsyncHandlerInterceptor.html[`AsyncHandlerInterceptor`] +{spring-framework-api}/web/servlet/AsyncHandlerInterceptor.html[`AsyncHandlerInterceptor`] for more details. `DeferredResult` provides `onTimeout(Runnable)` and `onCompletion(Runnable)` callbacks. -See the {api-spring-framework}/web/context/request/async/DeferredResult.html[javadoc of `DeferredResult`] +See the {spring-framework-api}/web/context/request/async/DeferredResult.html[javadoc of `DeferredResult`] for more details. `Callable` can be substituted for `WebAsyncTask` that exposes additional methods for timeout and completion callbacks. @@ -227,7 +227,7 @@ response, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/events") public ResponseBodyEmitter handle() { @@ -248,7 +248,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/events") fun handle() = ResponseBodyEmitter().apply { @@ -281,7 +281,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: @@ -289,7 +289,7 @@ stream from a controller, return `SseEmitter`, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter handle() { @@ -310,7 +310,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) fun handle() = SseEmitter().apply { @@ -348,7 +348,7 @@ return value type to do so, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/download") public StreamingResponseBody handle() { @@ -363,7 +363,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/download") fun handle() = StreamingResponseBody { @@ -399,7 +399,7 @@ Applications can also return `Flux` or `Observable>`. TIP: Spring MVC supports Reactor and RxJava through the -{api-spring-framework}/core/ReactiveAdapterRegistry.html[`ReactiveAdapterRegistry`] from +{spring-framework-api}/core/ReactiveAdapterRegistry.html[`ReactiveAdapterRegistry`] from `spring-core`, which lets it adapt from multiple reactive libraries. For streaming to the response, reactive back pressure is supported, but writes to the @@ -420,7 +420,7 @@ across multiple threads. The Micrometer https://github.com/micrometer-metrics/context-propagation#context-propagation-library[Context Propagation] library simplifies context propagation across threads, and across context mechanisms such as `ThreadLocal` values, -Reactor https://projectreactor.io/docs/core/release/reference/#context[context], +Reactor {reactor-site}/docs/core/release/reference/#context[context], GraphQL Java https://www.graphql-java.com/documentation/concerns/#context-objects[context], and others. @@ -445,9 +445,15 @@ directly. For example: } ---- -For more details, see the -https://micrometer.io/docs/contextPropagation[documentation] of the Micrometer Context -Propagation library. +The following `ThreadLocalAccessor` implementations are provided out of the box: + +* `LocaleContextThreadLocalAccessor` -- propagates `LocaleContext` via `LocaleContextHolder` +* `RequestAttributesThreadLocalAccessor` -- propagates `RequestAttributes` via `RequestContextHolder` + +The above are not registered automatically. You need to register them via `ContextRegistry.getInstance()` on startup. + +For more details, see the {micrometer-context-propagation-docs}/[documentation] of the +Micrometer Context Propagation library. @@ -500,8 +506,8 @@ The MVC configuration exposes the following options for asynchronous request pro You can configure the following: -* Default timeout value for async requests, which if not set, depends -on the underlying Servlet container. +* The default timeout value for async requests depends +on the underlying Servlet container, unless it is set explicitly. * `AsyncTaskExecutor` to use for blocking writes when streaming with xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] and for executing `Callable` instances returned from controller methods. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc index 245e370d5200..788a32d0fff9 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc @@ -5,14 +5,14 @@ A common requirement for REST services is to include details in the body of error responses. The Spring Framework supports the "Problem Details for HTTP APIs" -specification, https://www.rfc-editor.org/rfc/rfc7807.html[RFC 7807]. +specification, {rfc-site}/rfc9457[RFC 9457]. The following are the main abstractions for this support: -- `ProblemDetail` -- representation for an RFC 7807 problem detail; a simple container +- `ProblemDetail` -- representation for an RFC 9457 problem detail; a simple container for both standard fields defined in the spec, and for non-standard ones. - `ErrorResponse` -- contract to expose HTTP error response details including HTTP -status, response headers, and a body in the format of RFC 7807; this allows exceptions to +status, response headers, and a body in the format of RFC 9457; this allows exceptions to encapsulate and expose the details of how they map to an HTTP response. All Spring MVC exceptions implement this. - `ErrorResponseException` -- basic `ErrorResponse` implementation that others @@ -28,7 +28,7 @@ and any `ErrorResponseException`, and renders an error response with a body. [.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-render[See equivalent in the Reactive stack]# You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from -any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows: +any `@RequestMapping` method to render an RFC 9457 response. This is processed as follows: - The `status` property of `ProblemDetail` determines the HTTP status. - The `instance` property of `ProblemDetail` is set from the current URL path, if not @@ -37,20 +37,24 @@ already set. "application/problem+json" over "application/json" when rendering a `ProblemDetail`, and also falls back on it if no compatible media type is found. -To enable RFC 7807 responses for Spring WebFlux exceptions and for any +To enable RFC 9457 responses for Spring MVC exceptions and for any `ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an xref:web/webmvc/mvc-controller/ann-advice.adoc[@ControllerAdvice] in Spring configuration. The handler has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which includes all built-in web exceptions. You can add more exception handling methods, and use a protected method to map any exception to a `ProblemDetail`. +You can register `ErrorResponse` interceptors through the +xref:web/webmvc/mvc-config.adoc[MVC Config] with a `WebMvcConfigurer`. Use that to intercept +any RFC 9457 response and take some action. + [[mvc-ann-rest-exceptions-non-standard]] == Non-Standard Fields [.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-non-standard[See equivalent in the Reactive stack]# -You can extend an RFC 7807 response with non-standard fields in one of two ways. +You can extend an RFC 9457 response with non-standard fields in one of two ways. One, insert into the "properties" `Map` of `ProblemDetail`. When using the Jackson library, the Spring Framework registers `ProblemDetailJacksonMixin` that ensures this @@ -60,7 +64,7 @@ this `Map`. You can also extend `ProblemDetail` to add dedicated non-standard properties. The copy constructor in `ProblemDetail` allows a subclass to make it easy to be created -from an existing `ProblemDetail`. This could be done centrally, e.g. from an +from an existing `ProblemDetail`. This could be done centrally, for example, from an `@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the `ProblemDetail` of an exception into a subclass with the additional non-standard fields. @@ -79,9 +83,11 @@ message code arguments for the "detail" field. `ResponseEntityExceptionHandler` these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource] and updates the corresponding `ProblemDetail` fields accordingly. -The default strategy for message codes follows the pattern: +The default strategy for message codes is as follows: -`problemDetail.[type|title|detail].[fully qualified exception class name]` +* "type": `problemDetail.type.[fully qualified exception class name]` +* "title": `problemDetail.title.[fully qualified exception class name]` +* "detail": `problemDetail.[fully qualified exception class name][suffix]` An `ErrorResponse` may expose more than one message code, typically adding a suffix to the default message code. The table below lists message codes, and arguments for @@ -136,7 +142,7 @@ Message codes and arguments for each error are also resolved via `MessageSource` | `MethodArgumentNotValidException` | (default) | `+{0}+` the list of global errors, `+{1}+` the list of field errors. - Message codes and arguments for each error are also resolvedvia `MessageSource`. + Message codes and arguments for each error are also resolved via `MessageSource`. | `MissingRequestHeaderException` | (default) @@ -181,7 +187,7 @@ Message codes and arguments for each error are also resolved via `MessageSource` |=== NOTE: Unlike other exceptions, the message arguments for -`MethodArgumentValidException` and `HandlerMethodValidationException` are baed on a list of +`MethodArgumentValidException` and `HandlerMethodValidationException` are based on a list of `MessageSourceResolvable` errors that can also be customized through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource] resource bundle. See diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc index f011bad15660..5e4a6a782888 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc @@ -19,16 +19,16 @@ This section describes the HTTP caching-related options that are available in Sp == `CacheControl` [.small]#xref:web/webflux/caching.adoc#webflux-caching-cachecontrol[See equivalent in the Reactive stack]# -{api-spring-framework}/http/CacheControl.html[`CacheControl`] provides support for +{spring-framework-api}/http/CacheControl.html[`CacheControl`] provides support for configuring settings related to the `Cache-Control` header and is accepted as an argument in a number of places: -* {api-spring-framework}/web/servlet/mvc/WebContentInterceptor.html[`WebContentInterceptor`] -* {api-spring-framework}/web/servlet/support/WebContentGenerator.html[`WebContentGenerator`] +* {spring-framework-api}/web/servlet/mvc/WebContentInterceptor.html[`WebContentInterceptor`] +* {spring-framework-api}/web/servlet/support/WebContentGenerator.html[`WebContentGenerator`] * xref:web/webmvc/mvc-caching.adoc#mvc-caching-etag-lastmodified[Controllers] * xref:web/webmvc/mvc-caching.adoc#mvc-caching-static-resources[Static Resources] -While https://tools.ietf.org/html/rfc7234#section-5.2.2[RFC 7234] describes all possible +While {rfc-site}/rfc7234#section-5.2.2[RFC 7234] describes all possible directives for the `Cache-Control` response header, the `CacheControl` type takes a use case-oriented approach that focuses on the common scenarios: @@ -36,7 +36,7 @@ use case-oriented approach that focuses on the common scenarios: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Cache for an hour - "Cache-Control: max-age=3600" CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS); @@ -52,7 +52,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Cache for an hour - "Cache-Control: max-age=3600" val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS) @@ -91,7 +91,7 @@ settings to a `ResponseEntity`, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/book/{id}") public ResponseEntity showBook(@PathVariable Long id) { @@ -109,7 +109,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/book/{id}") fun showBook(@PathVariable id: Long): ResponseEntity { @@ -139,7 +139,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RequestMapping public String myHandleMethod(WebRequest request, Model model) { @@ -160,7 +160,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RequestMapping fun myHandleMethod(request: WebRequest, model: Model): String? { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-java.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-java.adoc index dbcdcaccb015..b4f501e7331c 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-java.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-java.adoc @@ -12,30 +12,7 @@ For advanced mode, you can remove `@EnableWebMvc` and extend directly from `DelegatingWebMvcConfiguration` instead of implementing `WebMvcConfigurer`, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - public class WebConfig extends DelegatingWebMvcConfiguration { - - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - class WebConfig : DelegatingWebMvcConfiguration() { - - // ... - } ----- -====== +include-code::./WebConfiguration[tag=snippet,indent=0] You can keep existing methods in `WebConfig`, but you can now also override bean declarations from the base class, and you can still have any number of other `WebMvcConfigurer` implementations on diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-xml.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-xml.adoc index bb7203372647..cc5a1130bc9f 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-xml.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-xml.adoc @@ -5,39 +5,7 @@ The MVC namespace does not have an advanced mode. If you need to customize a pro a bean that you cannot change otherwise, you can use the `BeanPostProcessor` lifecycle hook of the Spring `ApplicationContext`, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Component - public class MyPostProcessor implements BeanPostProcessor { - - public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { - // ... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Component - class MyPostProcessor : BeanPostProcessor { - - override fun postProcessBeforeInitialization(bean: Any, name: String): Any { - // ... - } - } ----- -====== - +include-code::./MyPostProcessor[tag=snippet,indent=0] Note that you need to declare `MyPostProcessor` as a bean, either explicitly in XML or by letting it be detected through a `` declaration. - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc index c209826dcbf8..3850a9931ba1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc @@ -13,59 +13,9 @@ strategy over path extensions. See xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-suffix-pattern-match[Suffix Match] and xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-rfd[Suffix Match and RFD] for more details. -In Java configuration, you can customize requested content type resolution, as the -following example shows: +You can customize requested content type resolution, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { - configurer.mediaType("json", MediaType.APPLICATION_JSON); - configurer.mediaType("xml", MediaType.APPLICATION_XML); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) { - configurer.mediaType("json", MediaType.APPLICATION_JSON) - configurer.mediaType("xml", MediaType.APPLICATION_XML) - } - } ----- -====== - - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - json=application/json - xml=application/xml - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/conversion.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/conversion.adoc index 35d0998934de..54a92fa56bb5 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/conversion.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/conversion.adoc @@ -4,121 +4,19 @@ [.small]#xref:web/webflux/config.adoc#webflux-config-conversion[See equivalent in the Reactive stack]# By default, formatters for various number and date types are installed, along with support -for customization via `@NumberFormat` and `@DateTimeFormat` on fields. +for customization via `@NumberFormat`, `@DurationFormat`, and `@DateTimeFormat` on fields +and parameters. -To register custom formatters and converters in Java config, use the following: +To register custom formatters and converters, use the following: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addFormatters(FormatterRegistry registry) { - // ... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addFormatters(registry: FormatterRegistry) { - // ... - } - } ----- -====== - -To do the same in XML config, use the following: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - - - - - - - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] By default Spring MVC considers the request Locale when parsing and formatting date values. This works for forms where dates are represented as Strings with "input" form fields. For "date" and "time" form fields, however, browsers use a fixed format defined in the HTML spec. For such cases date and time formatting can be customized as follows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addFormatters(FormatterRegistry registry) { - DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); - registrar.setUseIsoFormat(true); - registrar.registerFormatters(registry); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addFormatters(registry: FormatterRegistry) { - val registrar = DateTimeFormatterRegistrar() - registrar.setUseIsoFormat(true) - registrar.registerFormatters(registry) - } - } ----- -====== +include-code::./DateTimeWebConfiguration[tag=snippet,indent=0] NOTE: See xref:core/validation/format.adoc#format-FormatterRegistrar-SPI[the `FormatterRegistrar` SPI] and the `FormattingConversionServiceFactoryBean` for more information on when to use diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/customize.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/customize.adoc index dd7cf7e635e1..a42ea1388a44 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/customize.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/customize.adoc @@ -6,33 +6,7 @@ In Java configuration, you can implement the `WebMvcConfigurer` interface, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - // Implement configuration methods... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - // Implement configuration methods... - } ----- -====== - +include-code::./WebConfiguration[tag=snippet,indent=0] In XML, you can check attributes and sub-elements of ``. You can view the https://schema.spring.io/mvc/spring-mvc.xsd[Spring MVC XML schema] or use diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/default-servlet-handler.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/default-servlet-handler.adoc index 8251cd53528b..e983842eaf64 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/default-servlet-handler.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/default-servlet-handler.adoc @@ -15,93 +15,15 @@ lower than that of the `DefaultServletHttpRequestHandler`, which is `Integer.MAX The following example shows how to enable the feature by using the default setup: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable(); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { - configurer.enable() - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] The caveat to overriding the `/` Servlet mapping is that the `RequestDispatcher` for the default Servlet must be retrieved by name rather than by path. The `DefaultServletHttpRequestHandler` tries to auto-detect the default Servlet for the container at startup time, using a list of known names for most of the major Servlet -containers (including Tomcat, Jetty, GlassFish, JBoss, Resin, WebLogic, and WebSphere). +containers (including Tomcat, Jetty, GlassFish, JBoss, WebLogic, and WebSphere). If the default Servlet has been custom-configured with a different name, or if a different Servlet container is being used where the default Servlet name is unknown, then you must explicitly provide the default Servlet's name, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable("myCustomDefaultServlet"); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { - configurer.enable("myCustomDefaultServlet") - } - } ----- -====== - - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- - - - +include-code::./CustomDefaultServletConfiguration[tag=snippet,indent=0] 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 bec619f91f3d..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 @@ -3,50 +3,15 @@ [.small]#xref:web/webflux/config.adoc#webflux-config-enable[See equivalent in the Reactive stack]# -In Java configuration, you can use the `@EnableWebMvc` annotation to enable MVC -configuration, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig { - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig ----- -====== - -In XML configuration, you can use the `` element to enable MVC -configuration, as the following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - ----- +You can use the `@EnableWebMvc` annotation to enable MVC configuration with programmatic configuration, or `` with XML configuration, as the following example shows: + +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 xref:web/webmvc/mvc-servlet/special-bean-types.adoc[infrastructure beans] and adapts to dependencies diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc index 36c7d32956e5..d5354b4b297d 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc @@ -1,65 +1,14 @@ [[mvc-config-interceptors]] = Interceptors -In Java configuration, you can register interceptors to apply to incoming requests, as -the following example shows: +You can register interceptors to apply to incoming requests, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { +include-code::./WebConfiguration[tag=snippet,indent=0] - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new LocaleChangeInterceptor()); - registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**"); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addInterceptors(registry: InterceptorRegistry) { - registry.addInterceptor(LocaleChangeInterceptor()) - registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**") - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - - - ----- - -NOTE: Interceptors are not ideally suited as a security layer due to the potential -for a mismatch with annotated controller path matching, which can also match trailing -slashes and path extensions transparently, along with other path matching options. Many -of these options have been deprecated but the potential for a mismatch remains. -Generally, we recommend using Spring Security which includes a dedicated -https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html#mvc-requestmatcher[MvcRequestMatcher] -to align with Spring MVC path matching and also has a security firewall that blocks many -unwanted characters in URL paths. +WARNING: Interceptors are not ideally suited as a security layer due to the potential for +a mismatch with annotated controller path matching. Generally, we recommend using Spring +Security, or alternatively a similar approach integrated with the Servlet filter chain, +and applied as early as possible. NOTE: The XML config declares interceptors as `MappedInterceptor` beans, and those are in turn detected by any `HandlerMapping` bean, including those from other frameworks. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc index 1ead038524e0..a1e2d63303f2 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc @@ -5,79 +5,39 @@ You can set the `HttpMessageConverter` instances to use in Java configuration, replacing the ones used by default, by overriding -{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-[`configureMessageConverters()`]. +{spring-framework-api}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-[`configureMessageConverters()`]. You can also customize the list of configured message converters at the end by overriding -{api-spring-framework}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`]. +{spring-framework-api}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`]. TIP: In a Spring Boot application, the `WebMvcAutoConfiguration` adds any `HttpMessageConverter` beans it detects, in addition to default converters. Hence, in a -Boot application, prefer to use the -https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/message-converters.html[HttpMessageConverters] +Boot application, prefer to use the {spring-boot-docs-ref}/web/servlet.html#web.servlet.spring-mvc.message-converters[HttpMessageConverters] mechanism. Or alternatively, use `extendMessageConverters` to modify message converters at the end. -The following example adds XML and Jackson JSON converters with a customized -`ObjectMapper` instead of the default ones: +The following example adds XML and Jackson JSON converters with a customized `ObjectMapper` +instead of the default ones: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfiguration implements WebMvcConfigurer { - - @Override - public void configureMessageConverters(List> converters) { - Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() - .indentOutput(true) - .dateFormat(new SimpleDateFormat("yyyy-MM-dd")) - .modulesToInstall(new ParameterNamesModule()); - converters.add(new MappingJackson2HttpMessageConverter(builder.build())); - converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfiguration : WebMvcConfigurer { - - override fun configureMessageConverters(converters: MutableList>) { - val builder = Jackson2ObjectMapperBuilder() - .indentOutput(true) - .dateFormat(SimpleDateFormat("yyyy-MM-dd")) - .modulesToInstall(ParameterNamesModule()) - converters.add(MappingJackson2HttpMessageConverter(builder.build())) - converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())) ----- -====== +include-code::./WebConfiguration[tag=snippet,indent=0] In the preceding example, -{api-spring-framework}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`] +{spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`] is used to create a common configuration for both `MappingJackson2HttpMessageConverter` and `MappingJackson2XmlHttpMessageConverter` with indentation enabled, a customized date format, and the registration of -https://github.com/FasterXML/jackson-module-parameter-names[`jackson-module-parameter-names`], +{jackson-github-org}/jackson-module-parameter-names[`jackson-module-parameter-names`], Which adds support for accessing parameter names (a feature added in Java 8). This builder customizes Jackson's default properties as follows: -* https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled. -* https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled. +* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled. +* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled. It also automatically registers the following well-known modules if they are detected on the classpath: -* https://github.com/FasterXML/jackson-datatype-joda[jackson-datatype-joda]: Support for Joda-Time types. -* https://github.com/FasterXML/jackson-datatype-jsr310[jackson-datatype-jsr310]: Support for Java 8 Date and Time API types. -* https://github.com/FasterXML/jackson-datatype-jdk8[jackson-datatype-jdk8]: Support for other Java 8 types, such as `Optional`. -* https://github.com/FasterXML/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes. +* {jackson-github-org}/jackson-datatype-jsr310[jackson-datatype-jsr310]: Support for Java 8 Date and Time API types. +* {jackson-github-org}/jackson-datatype-jdk8[jackson-datatype-jdk8]: Support for other Java 8 types, such as `Optional`. +* {jackson-github-org}/jackson-module-kotlin[jackson-module-kotlin]: Support for Kotlin classes and data classes. NOTE: Enabling indentation with Jackson XML support requires https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.codehaus.woodstox%22%20AND%20a%3A%22woodstox-core-asl%22[`woodstox-core-asl`] @@ -86,30 +46,4 @@ dependency in addition to https://search.maven.org/#search%7Cga%7C1%7Ca%3A%22jac Other interesting Jackson modules are available: * https://github.com/zalando/jackson-datatype-money[jackson-datatype-money]: Support for `javax.money` types (unofficial module). -* https://github.com/FasterXML/jackson-datatype-hibernate[jackson-datatype-hibernate]: Support for Hibernate-specific types and properties (including lazy-loading aspects). - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - ----- - - - +* {jackson-github-org}/jackson-datatype-hibernate[jackson-datatype-hibernate]: Support for Hibernate-specific types and properties (including lazy-loading aspects). diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc index eff9db98d660..a0f33fee3ec1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc @@ -5,63 +5,8 @@ You can customize options related to path matching and treatment of the URL. For details on the individual options, see the -{api-spring-framework}/web/servlet/config/annotation/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. - -The following example shows how to customize path matching in Java configuration: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); - } - - private PathPatternParser patternParser() { - // ... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configurePathMatch(configurer: PathMatchConfigurer) { - configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) - } - - fun patternParser(): PathPatternParser { - //... - } - } ----- -====== - -The following example shows how to customize path matching in XML configuration: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - ----- - +{spring-framework-api}/web/servlet/config/annotation/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. +The following example shows how to customize path matching: +include-code::./WebConfiguration[tag=snippet,indent=0] 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 adb887831e97..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 @@ -4,7 +4,7 @@ [.small]#xref:web/webflux/config.adoc#webflux-config-static-resources[See equivalent in the Reactive stack]# This option provides a convenient way to serve static resources from a list of -{api-spring-framework}/core/io/Resource.html[`Resource`]-based locations. +{spring-framework-api}/core/io/Resource.html[`Resource`]-based locations. In the next example, given a request that starts with `/resources`, the relative path is used to find and serve static resources relative to `/public` under the web application @@ -13,59 +13,16 @@ expiration to ensure maximum use of the browser cache and a reduction in HTTP re made by the browser. The `Last-Modified` information is deduced from `Resource#lastModified` so that HTTP conditional requests are supported with `"Last-Modified"` headers. -The following listing shows how to do so with Java configuration: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addResourceHandlers(registry: ResourceHandlerRegistry) { - registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))) - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +The following listing shows how to do so: + +include-code::./WebConfiguration[tag=snippet,indent=0] See also xref:web/webmvc/mvc-caching.adoc#mvc-caching-static-resources[HTTP caching support for static resources]. The resource handler also supports a chain of -{api-spring-framework}/web/servlet/resource/ResourceResolver.html[`ResourceResolver`] implementations and -{api-spring-framework}/web/servlet/resource/ResourceTransformer.html[`ResourceTransformer`] implementations, +{spring-framework-api}/web/servlet/resource/ResourceResolver.html[`ResourceResolver`] implementations and +{spring-framework-api}/web/servlet/resource/ResourceTransformer.html[`ResourceTransformer`] implementations, which you can use to create a toolchain for working with optimized resources. You can use the `VersionResourceResolver` for versioned resource URLs based on an MD5 hash @@ -73,60 +30,9 @@ computed from the content, a fixed application version, or other. A `ContentVersionStrategy` (MD5 hash) is a good choice -- with some notable exceptions, such as JavaScript resources used with a module loader. -The following example shows how to use `VersionResourceResolver` in Java configuration: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/resources/**") - .addResourceLocations("/public/") - .resourceChain(true) - .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**")); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addResourceHandlers(registry: ResourceHandlerRegistry) { - registry.addResourceHandler("/resources/**") - .addResourceLocations("/public/") - .resourceChain(true) - .addResolver(VersionResourceResolver().addContentVersionStrategy("/**")) - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - - - - ----- +The following example shows how to use `VersionResourceResolver`: + +include-code::./VersionedConfiguration[tag=snippet,indent=0] You can then use `ResourceUrlProvider` to rewrite URLs and apply the full chain of resolvers and transformers -- for example, to insert versions. The MVC configuration provides a `ResourceUrlProvider` @@ -142,17 +48,16 @@ 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 -for fine-grained control, e.g. last-modified behavior and optimized resource resolution. +for fine-grained control, for example, last-modified behavior and optimized resource resolution. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/validation.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/validation.adoc index b307b8fc8c8c..b867977160fd 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/validation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/validation.adoc @@ -8,93 +8,15 @@ on the classpath (for example, Hibernate Validator), the `LocalValidatorFactoryB registered as a global xref:core/validation/validator.adoc[Validator] for use with `@Valid` and `@Validated` on controller method arguments. -In Java configuration, you can customize the global `Validator` instance, as the +You can customize the global `Validator` instance, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public Validator getValidator() { - // ... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun getValidator(): Validator { - // ... - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] Note that you can also register `Validator` implementations locally, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Controller - public class MyController { - - @InitBinder - protected void initBinder(WebDataBinder binder) { - binder.addValidators(new FooValidator()); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Controller - class MyController { - - @InitBinder - protected fun initBinder(binder: WebDataBinder) { - binder.addValidators(FooValidator()) - } - } ----- -====== +include-code::./MyController[tag=snippet,indent=0] TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and mark it with `@Primary` in order to avoid conflict with the one declared in the MVC configuration. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-controller.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-controller.adoc index 91811b7f415c..47d803b10c80 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-controller.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-controller.adoc @@ -5,47 +5,9 @@ This is a shortcut for defining a `ParameterizableViewController` that immediate forwards to a view when invoked. You can use it in static cases when there is no Java controller logic to run before the view generates the response. -The following example of Java configuration forwards a request for `/` to a view called `home`: +The following example forwards a request for `/` to a view called `home`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addViewControllers(ViewControllerRegistry registry) { - registry.addViewController("/").setViewName("home"); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addViewControllers(registry: ViewControllerRegistry) { - registry.addViewController("/").setViewName("home") - } - } ----- -====== - -The following example achieves the same thing as the preceding example, but with XML, by -using the `` element: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] If an `@RequestMapping` method is mapped to a URL for any HTTP method then a view controller cannot be used to handle the same URL. This is because a match by URL to an diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-resolvers.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-resolvers.adoc index cea23436efd8..5a0de6171d3f 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-resolvers.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-resolvers.adoc @@ -5,127 +5,12 @@ The MVC configuration simplifies the registration of view resolvers. -The following Java configuration example configures content negotiation view -resolution by using JSP and Jackson as a default `View` for JSON rendering: +The following example configures content negotiation view resolution by using JSP and Jackson as a +default `View` for JSON rendering: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.enableContentNegotiation(new MappingJackson2JsonView()); - registry.jsp(); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.enableContentNegotiation(MappingJackson2JsonView()) - registry.jsp() - } - } ----- -====== - - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] Note, however, that FreeMarker, Groovy Markup, and script templates also require -configuration of the underlying view technology. - -The MVC namespace provides dedicated elements. The following example works with FreeMarker: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - ----- - -In Java configuration, you can add the respective `Configurer` bean, -as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.enableContentNegotiation(new MappingJackson2JsonView()); - registry.freeMarker().cache(false); - } - - @Bean - public FreeMarkerConfigurer freeMarkerConfigurer() { - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setTemplateLoaderPath("/freemarker"); - return configurer; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.enableContentNegotiation(MappingJackson2JsonView()) - registry.freeMarker().cache(false) - } - - @Bean - fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { - setTemplateLoaderPath("/freemarker") - } - } ----- -====== - - +configuration of the underlying view technology. The following example works with FreeMarker: +include-code::./FreeMarkerConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller.adoc index 112361f7dba1..d759e804cfb4 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller.adoc @@ -13,7 +13,7 @@ The following example shows a controller defined by annotations: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class HelloController { @@ -28,7 +28,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.ui.set @@ -47,7 +47,7 @@ Kotlin:: In the preceding example, the method accepts a `Model` and returns a view name as a `String`, but many other options exist and are explained later in this chapter. -TIP: Guides and tutorials on https://spring.io/guides[spring.io] use the annotation-based +TIP: Guides and tutorials on {spring-site-guides}[spring.io] use the annotation-based programming model described in this section. 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 46726f71ad24..403db0bbf2a8 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 @@ -27,7 +27,7 @@ and handlers that they apply to. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Target all Controllers annotated with @RestController @ControllerAdvice(annotations = RestController.class) @@ -44,7 +44,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Target all Controllers annotated with @RestController @ControllerAdvice(annotations = [RestController::class]) @@ -62,7 +62,7 @@ Kotlin:: The selectors in the preceding example are evaluated at runtime and may negatively impact performance if used extensively. See the -{api-spring-framework}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] +{spring-framework-api}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] javadoc for more details. 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 73af9fa6ce46..e13037ded80d 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 @@ -6,43 +6,15 @@ `@Controller` and xref:web/webmvc/mvc-controller/ann-advice.adoc[@ControllerAdvice] classes can have `@ExceptionHandler` methods to handle exceptions from controller methods, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Controller - public class SimpleController { - - // ... - - @ExceptionHandler - public ResponseEntity handle(IOException ex) { - // ... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Controller - class SimpleController { - - // ... - - @ExceptionHandler - fun handle(ex: IOException): ResponseEntity { - // ... - } - } ----- -====== - -The exception may match against a top-level exception being propagated (e.g. a direct -`IOException` being thrown) or against a nested cause within a wrapper exception (e.g. + +include-code::./SimpleController[indent=0] + + +[[mvc-ann-exceptionhandler-exc]] +== Exception Mapping + +The exception may match against a top-level exception being propagated (for example, a direct +`IOException` being thrown) or against a nested cause within a wrapper exception (for example, an `IOException` wrapped inside an `IllegalStateException`). As of 5.3, this can match at arbitrary cause levels, whereas previously only an immediate cause was considered. @@ -54,54 +26,13 @@ is used to sort exceptions based on their depth from the thrown exception type. Alternatively, the annotation declaration may narrow the exception types to match, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @ExceptionHandler({FileSystemException.class, RemoteException.class}) - public ResponseEntity handle(IOException ex) { - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @ExceptionHandler(FileSystemException::class, RemoteException::class) - fun handle(ex: IOException): ResponseEntity { - // ... - } ----- -====== +include-code::./ExceptionController[tag=narrow,indent=0] You can even use a list of specific exception types with a very generic argument signature, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @ExceptionHandler({FileSystemException.class, RemoteException.class}) - public ResponseEntity handle(Exception ex) { - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @ExceptionHandler(FileSystemException::class, RemoteException::class) - fun handle(ex: Exception): ResponseEntity { - // ... - } ----- -====== +include-code::./ExceptionController[tag=general,indent=0] + [NOTE] ==== @@ -143,6 +74,25 @@ Support for `@ExceptionHandler` methods in Spring MVC is built on the `Dispatche level, xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[HandlerExceptionResolver] mechanism. + +[[mvc-ann-exceptionhandler-media]] +== Media Type Mapping +[.small]#xref:web/webflux/controller/ann-exceptions.adoc#webflux-ann-exceptionhandler-media[See equivalent in the Reactive stack]# + +In addition to exception types, `@ExceptionHandler` methods can also declare producible media types. +This allows to refine error responses depending on the media types requested by HTTP clients, typically in the "Accept" HTTP request header. + +Applications can declare producible media types directly on annotations, for the same exception type: + + +include-code::./MediaTypeController[tag=mediatype,indent=0] + +Here, methods handle the same exception type but will not be rejected as duplicates. +Instead, API clients requesting "application/json" will receive a JSON error, and browsers will get an HTML error view. +Each `@ExceptionHandler` annotation can declare several producible media types, +the content negotiation during the error handling phase will decide which content type will be used. + + [[mvc-ann-exceptionhandler-args]] == Method Arguments [.small]#xref:web/webflux/controller/ann-exceptions.adoc#webflux-ann-exceptionhandler-args[See equivalent in the Reactive stack]# @@ -228,11 +178,11 @@ level, xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[HandlerExceptionResolv See xref:web/webmvc/mvc-controller/ann-methods/responseentity.adoc[ResponseEntity]. | `ErrorResponse` -| To render an RFC 7807 error response with details in the body, +| 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 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | `String` @@ -271,7 +221,7 @@ see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | Any other return value | If a return value is not matched to any of the above and is not a simple type (as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]), + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]), by default, it is treated as a model attribute to be added to the model. If it is a simple type, it remains unresolved. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-initbinder.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-initbinder.adoc index 9562ac0f7bcc..eca8278f3505 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-initbinder.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-initbinder.adoc @@ -27,7 +27,7 @@ have, with the notable exception of `@ModelAttribute`. Typically, such methods h ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class FormController { @@ -46,7 +46,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class FormController { @@ -72,7 +72,7 @@ controller-specific `Formatter` implementations, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class FormController { @@ -89,7 +89,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class FormController { 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 d1509896fea8..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. @@ -135,7 +134,7 @@ and others) and is equivalent to `required=false`. | Any other argument | If a method argument is not matched to any of the earlier values in this table and it is a simple type (as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]), + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]), it is resolved as a `@RequestParam`. Otherwise, it is resolved as a `@ModelAttribute`. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/cookievalue.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/cookievalue.adoc index d61859b5a15b..f56b52b967eb 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/cookievalue.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/cookievalue.adoc @@ -19,7 +19,7 @@ The following example shows how to get the cookie value: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/demo") public void handle(@CookieValue("JSESSIONID") String cookie) { <1> @@ -30,7 +30,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/demo") fun handle(@CookieValue("JSESSIONID") cookie: String) { // <1> diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/httpentity.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/httpentity.adoc index 024f99b14919..80e75a0e7d37 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/httpentity.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/httpentity.adoc @@ -10,7 +10,7 @@ container object that exposes request headers and body. The following listing sh ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") public void handle(HttpEntity entity) { @@ -20,7 +20,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") fun handle(entity: HttpEntity) { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/jackson.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/jackson.adoc index 3ea43c3e9713..b8c24d6641ac 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/jackson.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/jackson.adoc @@ -8,7 +8,7 @@ Spring offers support for the Jackson JSON library. [.small]#xref:web/webflux/controller/ann-methods/jackson.adoc#webflux-ann-jsonview[See equivalent in the Reactive stack]# Spring MVC provides built-in support for -https://www.baeldung.com/jackson-json-view-annotation[Jackson's Serialization Views], +{baeldung-blog}/jackson-json-view-annotation[Jackson's Serialization Views], which allow rendering only a subset of all fields in an `Object`. To use it with `@ResponseBody` or `ResponseEntity` controller methods, you can use Jackson's `@JsonView` annotation to activate a serialization view class, as the following example shows: @@ -17,7 +17,7 @@ which allow rendering only a subset of all fields in an `Object`. To use it with ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController public class UserController { @@ -59,7 +59,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController class UserController { @@ -89,7 +89,7 @@ wrap the return value with `MappingJacksonValue` and use it to supply the serial ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController public class UserController { @@ -106,7 +106,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController class UserController { @@ -128,7 +128,7 @@ to the model, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class UserController extends AbstractController { @@ -144,7 +144,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class UserController : AbstractController() { 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 83171fc4f036..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 @@ -3,7 +3,7 @@ [.small]#xref:web/webflux/controller/ann-methods/matrix-variables.adoc[See equivalent in the Reactive stack]# -https://tools.ietf.org/html/rfc3986#section-3.3[RFC 3986] discusses name-value pairs in +{rfc-site}/rfc3986#section-3.3[RFC 3986] discusses name-value pairs in path segments. In Spring MVC, we refer to those as "`matrix variables`" based on an https://www.w3.org/DesignIssues/MatrixURIs.html["`old post`"] by Tim Berners-Lee, but they can be also be referred to as URI path parameters. @@ -22,7 +22,7 @@ The following example uses a matrix variable: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // GET /pets/42;q=11;r=22 @@ -36,7 +36,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // GET /pets/42;q=11;r=22 @@ -57,7 +57,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // GET /owners/42;q=11/pets/21;q=22 @@ -73,7 +73,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // GET /owners/42;q=11/pets/21;q=22 @@ -95,7 +95,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // GET /pets/42 @@ -108,7 +108,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // GET /pets/42 @@ -126,7 +126,7 @@ To get all matrix variables, you can use a `MultiValueMap`, as the following exa ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // GET /owners/42;q=11;r=12/pets/21;q=22;s=23 @@ -142,7 +142,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // GET /owners/42;q=11;r=12/pets/21;q=22;s=23 @@ -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 02e314974ab1..7a04b5ba7f13 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 @@ -3,14 +3,14 @@ [.small]#xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Reactive stack]# -The `@ModelAttribute` method parameter annotation binds request parameters onto a model -object. For example: +The `@ModelAttribute` method parameter annotation binds request parameters, URI path variables, +and request headers onto a model object. For example: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute Pet pet) { // <1> @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") fun processSubmit(@ModelAttribute pet: Pet): String { // <1> @@ -31,7 +31,11 @@ fun processSubmit(@ModelAttribute pet: Pet): String { // <1> <1> Bind to an instance of `Pet`. ====== -The `Pet` instance may be: +Request parameters are a Servlet API concept that includes form data from the request body, +and query parameters. URI variables and headers are also included, but only if they don't +override request parameters with the same name. Dashes are stripped from header names. + +The `Pet` instance above may be: * Accessed from the model where it could have been added by a xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method]. @@ -54,7 +58,7 @@ registered `Converter` that perhaps retrieves it from a persist ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PutMapping("/accounts/{account}") public String save(@ModelAttribute("account") Account account) { // <1> @@ -64,7 +68,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PutMapping("/accounts/{account}") fun save(@ModelAttribute("account") account: Account): String { // <1> @@ -89,7 +93,7 @@ When using constructor binding, you can customize request parameter names throug ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class Account { @@ -102,7 +106,7 @@ Java:: ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Account(@BindParam("first-name") val firstName: String) ---- @@ -112,6 +116,10 @@ NOTE: The `@BindParam` may also be placed on the fields that correspond to const parameters. While `@BindParam` is supported out of the box, you can also use a different annotation by setting a `DataBinder.NameResolver` on `DataBinder` +Constructor binding supports `List`, `Map`, and array arguments either converted from +a single string, for example, comma-separated list, or based on indexed keys such as +`accounts[2].name` or `account[KEY].name`. + In some cases, you may want access to a model attribute without data binding. For such cases, you can inject the `Model` into the controller and access it directly or, alternatively, set `@ModelAttribute(binding=false)`, as the following example shows: @@ -120,7 +128,7 @@ alternatively, set `@ModelAttribute(binding=false)`, as the following example sh ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute public AccountForm setUpForm() { @@ -142,7 +150,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute fun setUpForm(): AccountForm { @@ -171,7 +179,7 @@ in order to handle such errors in the controller method. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { // <1> @@ -185,7 +193,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> @@ -207,7 +215,7 @@ xref:web/webmvc/mvc-config/validation.adoc[Spring validation]. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1> @@ -221,7 +229,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> @@ -242,5 +250,10 @@ xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]. TIP: Using `@ModelAttribute` is optional. By default, any parameter that is not a simple value type as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty] -_AND_ that is not resolved by any other argument resolver is treated as an `@ModelAttribute`. +{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty] +_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`. + +WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute` +support described above does not allow proper ahead-of-time inference of related data +binding reflection hints. As a consequence, it is recommended to explicitly annotate +method parameters with `@ModelAttribute` for use in a GraalVM native image. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc index 5e4addcb3a26..55c11bcbfa53 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc @@ -12,7 +12,7 @@ file: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class FileUploadController { @@ -33,7 +33,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class FileUploadController { @@ -72,7 +72,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class MyForm { @@ -100,7 +100,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyForm(val name: String, val file: MultipartFile, ...) @@ -153,7 +153,7 @@ xref:integration/rest-clients.adoc#rest-message-conversion[HttpMessageConverter] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") public String handle(@RequestPart("meta-data") MetaData metadata, @@ -164,7 +164,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") fun handle(@RequestPart("meta-data") metadata: MetaData, @@ -185,7 +185,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") public String handle(@Valid @RequestPart("meta-data") MetaData metadata, Errors errors) { @@ -195,7 +195,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") fun handle(@Valid @RequestPart("meta-data") metadata: MetaData, errors: Errors): String { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/redirecting-passing-data.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/redirecting-passing-data.adoc index 5ed5b89b4d15..645f2a25ae15 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/redirecting-passing-data.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/redirecting-passing-data.adoc @@ -30,7 +30,7 @@ through `Model` or `RedirectAttributes`. The following example shows how to defi ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/files/{path}") public String upload(...) { @@ -41,7 +41,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/files/{path}") fun upload(...): String { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestattrib.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestattrib.adoc index 110b415b4c08..76da7fff8828 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestattrib.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestattrib.adoc @@ -11,7 +11,7 @@ or `HandlerInterceptor`): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/") public String handle(@RequestAttribute Client client) { // <1> @@ -22,7 +22,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/") fun handle(@RequestAttribute client: Client): String { // <1> diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestbody.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestbody.adoc index 9cdc1e8fb073..afb1509394d5 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestbody.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestbody.adoc @@ -11,7 +11,7 @@ The following example uses a `@RequestBody` argument: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") public void handle(@RequestBody Account account) { @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") fun handle(@RequestBody account: Account) { @@ -34,6 +34,10 @@ Kotlin:: You can use the xref:web/webmvc/mvc-config/message-converters.adoc[Message Converters] option of the xref:web/webmvc/mvc-config.adoc[MVC Config] to configure or customize message conversion. +NOTE: Form data should be read using xref:web/webmvc/mvc-controller/ann-methods/requestparam.adoc[`@RequestParam`], +not with `@RequestBody` which can't always be used reliably since in the Servlet API, request parameter +access causes the request body to be parsed, and it can't be read again. + You can use `@RequestBody` in combination with `jakarta.validation.Valid` or Spring's `@Validated` annotation, both of which cause Standard Bean Validation to be applied. By default, validation errors cause a `MethodArgumentNotValidException`, which is turned @@ -45,7 +49,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") public void handle(@Valid @RequestBody Account account, Errors errors) { @@ -55,7 +59,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/accounts") fun handle(@Valid @RequestBody account: Account, errors: Errors) { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestheader.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestheader.adoc index a63151aa66cf..99c1bcd0f6a0 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestheader.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestheader.adoc @@ -25,7 +25,7 @@ The following example gets the value of the `Accept-Encoding` and `Keep-Alive` h ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/demo") public void handle( @@ -39,7 +39,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/demo") fun handle( diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestparam.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestparam.adoc index ae5e3a0ed4c7..839e0f57eab5 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestparam.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestparam.adoc @@ -12,7 +12,7 @@ The following example shows how to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/pets") @@ -35,7 +35,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.ui.set @@ -61,7 +61,7 @@ Kotlin:: By default, method parameters that use this annotation are required, but you can specify that a method parameter is optional by setting the `@RequestParam` annotation's `required` flag to -`false` or by declaring the argument with an `java.util.Optional` wrapper. +`false` or by declaring the argument with a `java.util.Optional` wrapper. Type conversion is automatically applied if the target method parameter type is not `String`. See xref:web/webmvc/mvc-controller/ann-methods/typeconversion.adoc[Type Conversion]. @@ -72,10 +72,52 @@ values for the same parameter name. When an `@RequestParam` annotation is declared as a `Map` or `MultiValueMap`, without a parameter name specified in the annotation, then the map is populated with the request parameter values for each given parameter name. +The following example shows how to do so with form data processing: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + @RequestMapping("/pets") + class EditPetForm { + + // ... + + @PostMapping(path = "/process", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public String processForm(@RequestParam MultiValueMap params) { + // ... + } + + // ... + } +---- +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Controller + @RequestMapping("/pets") + class EditPetForm { + + // ... + + @PostMapping("/process", consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE]) + fun processForm(@RequestParam params: MultiValueMap): String { + // ... + } + + // ... + + } +---- +====== Note that use of `@RequestParam` is optional (for example, to set its attributes). By default, any argument that is a simple value type (as determined by -{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) +{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]) and is not resolved by any other argument resolver, is treated as if it were annotated with `@RequestParam`. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc index 1cd1816e5d3c..8d2d827cee38 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responsebody.adoc @@ -12,7 +12,7 @@ The following listing shows an example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/accounts/{id}") @ResponseBody @@ -23,7 +23,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/accounts/{id}") @ResponseBody @@ -37,6 +37,13 @@ Kotlin:: all controller methods. This is the effect of `@RestController`, which is nothing more than a meta-annotation marked with `@Controller` and `@ResponseBody`. +A `Resource` object can be returned for file content, copying the `InputStream` +content of the provided resource to the response `OutputStream`. Note that the +`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably +close it after it has been copied to the response. If you are using `InputStreamResource` +for such a purpose, make sure to construct it with an on-demand `InputStreamSource` +(for example, through a lambda expression that retrieves the actual `InputStream`). + You can use `@ResponseBody` with reactive types. See xref:web/webmvc/mvc-ann-async.adoc[Asynchronous Requests] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] for more details. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc index f311386cabb4..f61a9878b1f5 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/responseentity.adoc @@ -9,7 +9,7 @@ ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/something") public ResponseEntity handle() { @@ -21,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/something") fun handle(): ResponseEntity { @@ -32,6 +32,18 @@ Kotlin:: ---- ====== +The body will usually be provided as a value object to be rendered to a corresponding +response representation (for example, JSON) by one of the registered `HttpMessageConverters`. + +A `ResponseEntity` can be returned for file content, copying the `InputStream` +content of the provided resource to the response `OutputStream`. Note that the +`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably +close it after it has been copied to the response. If you are using `InputStreamResource` +for such a purpose, make sure to construct it with an on-demand `InputStreamSource` +(for example, through a lambda expression that retrieves the actual `InputStream`). Also, custom +subclasses of `InputStreamResource` are only supported in combination with a custom +`contentLength()` implementation which avoids consuming the stream for that purpose. + Spring MVC supports using a single value xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive type] to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive types for the body. This allows the following types of async responses: 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 266d6ee1aaae..557de2db3ca2 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 @@ -23,11 +23,11 @@ supported for all return values. | For returning a response with headers and no body. | `ErrorResponse` -| To render an RFC 7807 error response with details in the body, +| 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 7807 error response with details in the body, +| To render an RFC 9457 error response with details in the body, see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | `String` @@ -56,6 +56,10 @@ supported for all return values. | `ModelAndView` object | The view and model attributes to use and, optionally, a response status. +| `FragmentsRendering`, `Collection` +| For rendering one or more fragments each with its own view and model. + See xref:web/webmvc-view/mvc-fragments.adoc[HTML Fragments] for more details. + | `void` | A method with a `void` return type (or `null` return value) is considered to have fully handled the response if it also has a `ServletResponse`, an `OutputStream` argument, or @@ -89,16 +93,16 @@ supported for all return values. `ResponseEntity`. See xref:web/webmvc/mvc-ann-async.adoc[Asynchronous Requests] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[HTTP Streaming]. | Reactor and other reactive types registered via `ReactiveAdapterRegistry` -| A single value type, e.g. `Mono`, is comparable to returning `DeferredResult`. - A multi-value type, e.g. `Flux`, may be treated as a stream depending on the requested - media type, e.g. "text/event-stream", "application/json+stream", or otherwise is +| A single value type, for example, `Mono`, is comparable to returning `DeferredResult`. + A multi-value type, for example, `Flux`, may be treated as a stream depending on the requested + media type, for example, "text/event-stream", "application/json+stream", or otherwise is collected to a List and rendered as a single value. See xref:web/webmvc/mvc-ann-async.adoc[Asynchronous Requests] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types]. | Other return values | If a return value remains unresolved in any other way, it is treated as a model attribute, unless it is a simple type as determined by - {api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], + {spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty], in which case it remains unresolved. |=== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/sessionattribute.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/sessionattribute.adoc index 726952dc5643..5e7754d3c4ef 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/sessionattribute.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/sessionattribute.adoc @@ -12,7 +12,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RequestMapping("/") public String handle(@SessionAttribute User user) { <1> @@ -23,7 +23,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RequestMapping("/") fun handle(@SessionAttribute user: User): String { // <1> diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/sessionattributes.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/sessionattributes.adoc index b2ea7ce9e33d..f9497c08bb44 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/sessionattributes.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/sessionattributes.adoc @@ -15,7 +15,7 @@ The following example uses the `@SessionAttributes` annotation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller @SessionAttributes("pet") // <1> @@ -27,7 +27,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller @SessionAttributes("pet") // <1> @@ -47,7 +47,7 @@ storage, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller @SessionAttributes("pet") // <1> @@ -70,7 +70,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller @SessionAttributes("pet") // <1> diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/typeconversion.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/typeconversion.adoc index fbaf33f89955..76f10b3d40e8 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/typeconversion.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/typeconversion.adoc @@ -26,7 +26,7 @@ method intends to accept a null value as well, either declare your argument as ` or mark it as `required=false` in the corresponding `@RequestParam`, etc. annotation. This is a best practice and the recommended solution for regressions encountered in a 5.3 upgrade. -Alternatively, you may specifically handle e.g. the resulting `MissingPathVariableException` +Alternatively, you may specifically handle, for example, the resulting `MissingPathVariableException` in the case of a required `@PathVariable`. A null value after conversion will be treated like an empty original value, so the corresponding `Missing...Exception` variants will be thrown. ==== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-modelattrib-methods.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-modelattrib-methods.adoc index 0f58a97dd8d6..c0e28ba010f4 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-modelattrib-methods.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-modelattrib-methods.adoc @@ -28,7 +28,7 @@ The following example shows a `@ModelAttribute` method: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute public void populateModel(@RequestParam String number, Model model) { @@ -39,7 +39,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute fun populateModel(@RequestParam number: String, model: Model) { @@ -55,7 +55,7 @@ The following example adds only one attribute: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute public Account addAccount(@RequestParam String number) { @@ -65,7 +65,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ModelAttribute fun addAccount(@RequestParam number: String): Account { @@ -76,7 +76,7 @@ Kotlin:: NOTE: When a name is not explicitly specified, a default name is chosen based on the `Object` -type, as explained in the javadoc for {api-spring-framework}/core/Conventions.html[`Conventions`]. +type, as explained in the javadoc for {spring-framework-api}/core/Conventions.html[`Conventions`]. You can always assign an explicit name by using the overloaded `addAttribute` method or through the `name` attribute on `@ModelAttribute` (for a return value). @@ -90,7 +90,7 @@ unless the return value is a `String` that would otherwise be interpreted as a v ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/accounts/{id}") @ModelAttribute("myAccount") @@ -102,7 +102,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/accounts/{id}") @ModelAttribute("myAccount") 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 932c2f6b1419..5ed0bfa38104 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 @@ -30,13 +30,19 @@ arguably, most controller methods should be mapped to a specific HTTP method ver using `@RequestMapping`, which, by default, matches to all HTTP methods. A `@RequestMapping` is still needed at the class level to express shared mappings. +NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping` +annotations that are declared on the same element (class, interface, or method). If +multiple `@RequestMapping` annotations are detected on the same element, a warning will +be logged, and only the first mapping will be used. This also applies to composed +`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc. + The following example has type and method level mappings: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/persons") @@ -57,7 +63,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/persons") @@ -99,13 +105,13 @@ default from version 6.0. See xref:web/webmvc/mvc-config/path-matching.adoc[MVC customizations of path matching options. `PathPattern` supports the same pattern syntax as `AntPathMatcher`. In addition, it also -supports the capturing pattern, e.g. `+{*spring}+`, for matching 0 or more path segments +supports the capturing pattern, for example, `+{*spring}+`, for matching 0 or more path segments at the end of a path. `PathPattern` also restricts the use of `+**+` for matching multiple path segments such that it's only allowed at the end of a pattern. This eliminates many cases of ambiguity when choosing the best matching pattern for a given request. For full pattern syntax please refer to -{api-spring-framework}/web/util/pattern/PathPattern.html[PathPattern] and -{api-spring-framework}/util/AntPathMatcher.html[AntPathMatcher]. +{spring-framework-api}/web/util/pattern/PathPattern.html[PathPattern] and +{spring-framework-api}/util/AntPathMatcher.html[AntPathMatcher]. Some example patterns: @@ -113,7 +119,7 @@ Some example patterns: * `+"/resources/*.png"+` - match zero or more characters in a path segment * `+"/resources/**"+` - match multiple path segments * `+"/projects/{project}/versions"+` - match a path segment and capture it as a variable -* `+"/projects/{project:[a-z]+}/versions"+` - match and capture a variable with a regex +* `++"/projects/{project:[a-z]+}/versions"++` - match and capture a variable with a regex Captured URI variables can be accessed with `@PathVariable`. For example: @@ -121,7 +127,7 @@ Captured URI variables can be accessed with `@PathVariable`. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/owners/{ownerId}/pets/{petId}") public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { @@ -131,7 +137,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/owners/{ownerId}/pets/{petId}") fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet { @@ -147,7 +153,7 @@ You can declare URI variables at the class and method levels, as the following e ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/owners/{ownerId}") @@ -162,7 +168,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/owners/{ownerId}") @@ -193,7 +199,7 @@ extracts the name, version, and file extension: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) { @@ -203,7 +209,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") fun handle(@PathVariable name: String, @PathVariable version: String, @PathVariable ext: String) { @@ -226,8 +232,8 @@ some external configuration. When multiple patterns match a URL, the best match must be selected. This is done with one of the following depending on whether use of parsed `PathPattern` is enabled for use or not: -* {api-spring-framework}/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR[`PathPattern.SPECIFICITY_COMPARATOR`] -* {api-spring-framework}/util/AntPathMatcher.html#getPatternComparator-java.lang.String-[`AntPathMatcher.getPatternComparator(String path)`] +* {spring-framework-api}/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR[`PathPattern.SPECIFICITY_COMPARATOR`] +* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator-java.lang.String-[`AntPathMatcher.getPatternComparator(String path)`] Both help to sort patterns with more specific ones on top. A pattern is more specific if it has a lower count of URI variables (counted as 1), single wildcards (counted as 1), @@ -242,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, e.g. 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 @@ -296,7 +272,7 @@ Many common path extensions are allowed as safe by default. Applications with cu negotiation to avoid having a `Content-Disposition` header added for those extensions. See xref:web/webmvc/mvc-config/content-negotiation.adoc[Content Types]. -See https://pivotal.io/security/cve-2015-5211[CVE-2015-5211] for additional +See {spring-site-cve}/cve-2015-5211[CVE-2015-5211] for additional recommendations related to RFD. @@ -311,7 +287,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @PostMapping(path = "/pets", consumes = "application/json") // <1> public void addPet(@RequestBody Pet pet) { @@ -322,7 +298,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/pets", consumes = ["application/json"]) // <1> fun addPet(@RequestBody pet: Pet) { @@ -354,7 +330,7 @@ content types that a controller method produces, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping(path = "/pets/{petId}", produces = "application/json") // <1> @ResponseBody @@ -366,7 +342,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/pets/{petId}", produces = ["application/json"]) // <1> @ResponseBody @@ -400,7 +376,7 @@ specific value (`myParam=myValue`). The following example shows how to test for ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping(path = "/pets/{petId}", params = "myParam=myValue") // <1> public void findPet(@PathVariable String petId) { @@ -411,7 +387,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/pets/{petId}", params = ["myParam=myValue"]) // <1> fun findPet(@PathVariable petId: String) { @@ -427,7 +403,7 @@ You can also use the same with request header conditions, as the following examp ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") // <1> public void findPet(@PathVariable String petId) { @@ -438,7 +414,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/pets/{petId}", headers = ["myHeader=myValue"]) // <1> fun findPet(@PathVariable petId: String) { @@ -462,11 +438,6 @@ transparently for request mapping. Controller methods do not need to change. A response wrapper, applied in `jakarta.servlet.http.HttpServlet`, ensures a `Content-Length` header is set to the number of bytes written (without actually writing to the response). -`@GetMapping` (and `@RequestMapping(method=HttpMethod.GET)`) are implicitly mapped to -and support HTTP HEAD. An HTTP HEAD request is processed as if it were HTTP GET except -that, instead of writing the body, the number of bytes are counted and the `Content-Length` -header is set. - By default, HTTP OPTIONS is handled by setting the `Allow` response header to the list of HTTP methods listed in all `@RequestMapping` methods that have matching URL patterns. @@ -491,8 +462,14 @@ attributes with a narrower, more specific purpose. `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, and `@PatchMapping` are examples of composed annotations. They are provided because, arguably, most controller methods should be mapped to a specific HTTP method versus using `@RequestMapping`, -which, by default, matches to all HTTP methods. If you need an example of composed -annotations, look at how those are declared. +which, by default, matches to all HTTP methods. If you need an example of how to implement +a composed annotation, look at how those are declared. + +NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping` +annotations that are declared on the same element (class, interface, or method). If +multiple `@RequestMapping` annotations are detected on the same element, a warning will +be logged, and only the first mapping will be used. This also applies to composed +`@RequestMapping` annotations such as `@GetMapping`, `@PostMapping`, etc. Spring MVC also supports custom request-mapping attributes with custom request-matching logic. This is a more advanced option that requires subclassing @@ -512,7 +489,7 @@ under different URLs. The following example registers a handler method: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class MyConfig { @@ -537,7 +514,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class MyConfig { @@ -562,10 +539,17 @@ Kotlin:: == `@HttpExchange` [.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-httpexchange-annotation[See equivalent in the Reactive stack]# -As an alternative to `@RequestMapping`, you can also handle requests with `@HttpExchange` -methods. Such methods are declared on an -xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] and can be used as -a client via `HttpServiceProxyFactory` or implemented by a server `@Controller`. +While the main purpose of `@HttpExchange` is to abstract HTTP client code with a +generated proxy, the +xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] on which +such annotations are placed is a contract neutral to client vs server use. +In addition to simplifying client code, there are also cases where an HTTP Interface +may be a convenient way for servers to expose their API for client access. This leads +to increased coupling between client and server and is often not a good choice, +especially for public API's, but may be exactly the goal for an internal API. +It is an approach commonly used in Spring Cloud, and it is why `@HttpExchange` is +supported as an alternative to `@RequestMapping` for server side handling in +controller classes. For example: @@ -573,18 +557,25 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - @RestController @HttpExchange("/persons") - class PersonController { + interface PersonService { @GetExchange("/{id}") + Person getPerson(@PathVariable Long id); + + @PostExchange + void add(@RequestBody Person person); + } + + @RestController + class PersonController implements PersonService { + public Person getPerson(@PathVariable Long id) { // ... } - @PostExchange @ResponseStatus(HttpStatus.CREATED) public void add(@RequestBody Person person) { // ... @@ -594,32 +585,45 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - @RestController @HttpExchange("/persons") - class PersonController { + interface PersonService { @GetExchange("/{id}") - fun getPerson(@PathVariable id: Long): Person { + fun getPerson(@PathVariable id: Long): Person + + @PostExchange + fun add(@RequestBody person: Person) + } + + @RestController + class PersonController : PersonService { + + override fun getPerson(@PathVariable id: Long): Person { // ... } - @PostExchange @ResponseStatus(HttpStatus.CREATED) - fun add(@RequestBody person: Person) { + override fun add(@RequestBody person: Person) { // ... } } ---- ====== -There some differences between `@HttpExchange` and `@RequestMapping` since the -former needs to remain suitable for client and server use. For example, while -`@RequestMapping` can be declared to handle any number of paths and each path can -be a pattern, `@HttpExchange` must be declared with a single, concrete path. There are -also differences in the supported method parameters. Generally, `@HttpExchange` supports -a subset of method parameters that `@RequestMapping` does, excluding any parameters that -are server side only. For details see the list of supported method parameters for -xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[HTTP interface] and for +`@HttpExchange` and `@RequestMapping` have differences. +`@RequestMapping` can map to any number of requests by path patterns, HTTP methods, +and more, while `@HttpExchange` declares a single endpoint with a concrete HTTP method, +path, and content types. + +For method parameters and returns values, generally, `@HttpExchange` supports a +subset of the method parameters that `@RequestMapping` does. Notably, it excludes any +server-side specific parameter types. For details, see the list for +xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and xref:web/webmvc/mvc-controller/ann-methods/arguments.adoc[@RequestMapping]. + +`@HttpExchange` also supports a `headers()` parameter which accepts `"name=value"`-like +pairs like in `@RequestMapping(headers={})` on the client side. On the server side, +this extends to the full syntax that +xref:#mvc-ann-requestmapping-params-and-headers[`@RequestMapping`] supports. 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 a1528b55343f..34cf05e99df2 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 @@ -3,28 +3,40 @@ [.small]#xref:web/webflux/controller/ann-validation.adoc[See equivalent in the Reactive stack]# -Spring MVC has built-in xref:core/validation/validator.adoc[Validation] support for -`@RequestMapping` methods, including the option to use -xref:core/validation/beanvalidation.adoc[Java Bean Validation]. -The validation support works on two levels. +Spring MVC has built-in xref:core/validation/validator.adoc[validation] for +`@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation]. +Validation may be applied at one of two levels: -First, method parameters such as -xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], +1. xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], xref:web/webmvc/mvc-controller/ann-methods/requestbody.adoc[@RequestBody], and -xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] do perform -validation if annotated with Jakarta's `@Valid` or Spring's `@Validated` annotation, and -raise `MethodArgumentNotValidException` in case of validation errors. If you want to handle -the errors in the controller method instead, you can declare an `Errors` or `BindingResult` -method parameter immediately after the validated parameter. - -Second, if https://beanvalidation.org/[Java Bean Validation] is present _AND_ other method -parameters, e.g. `@RequestHeader`, `@RequestParam`, `@PathVariable` have `@Constraint` -annotations, then method validation is applied to all method arguments, raising -`HandlerMethodValidationException` in case of validation errors. You can still declare an -`Errors` or `BindingResult` after an `@Valid` method parameter, and handle validation -errors within the controller method, as long as there are no validation errors on other -method arguments. Method validation is also applied to the return value if the method -is annotated with `@Valid` or has other `@Constraint` annotations. +xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] argument +resolvers validate a method argument individually if the method parameter is annotated +with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or +`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be +discussed next). The exception raised in this case is `MethodArgumentNotValidException`. + +2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared +directly on method parameters, or on the method (for the return value), then method +validation must be applied, and that supersedes validation at the method argument level +because method validation covers both method parameter constraints and nested constraints +via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`. + +Applications must handle both `MethodArgumentNotValidException` and +`HandlerMethodValidationException` as either may be raised depending on the controller +method signature. The two exceptions, however are designed to be very similar, and can be +handled with almost identical code. The main difference is that the former is for a single +object while the latter is for a list of method parameters. + +NOTE: `@Valid` is not a constraint annotation, but rather for nested constraints within +an Object. Therefore, by itself `@Valid` does not lead to method validation. `@NotNull` +on the other hand is a constraint, and adding it to an `@Valid` parameter leads to method +validation. For nullability specifically, you may also use the `required` flag of +`@RequestBody` or `@ModelAttribute`. + +Method validation may be used in combination with `Errors` or `BindingResult` method +parameters. However, the controller method is called only if all validation errors are on +method parameters with an `Errors` immediately after. If there are validation errors on +any other method parameter then `HandlerMethodValidationException` is raised. You can configure a `Validator` globally through the xref:web/webmvc/mvc-config/validation.adoc[WebMvc config], or locally through an @@ -45,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: @@ -53,7 +65,7 @@ methods by controller method parameter type: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HandlerMethodValidationException ex = ... ; @@ -83,7 +95,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // HandlerMethodValidationException val ex @@ -109,4 +121,4 @@ Kotlin:: } }) ---- -====== \ No newline at end of file +====== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc index b6495f54dd44..493d1d74d5f2 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc @@ -12,54 +12,7 @@ annotated class, indicating its role as a web component. To enable auto-detection of such `@Controller` beans, you can add component scanning to your Java configuration, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @ComponentScan("org.example.web") - public class WebConfig { - - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @ComponentScan("org.example.web") - class WebConfig { - - // ... - } ----- -====== - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] `@RestController` is a xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[composed annotation] that is itself meta-annotated with `@Controller` and `@ResponseBody` to indicate a controller whose 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 17a10e537f7f..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 -https://github.com/spring-projects/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. +{spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page]. 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 5b6aca1f70ad..9a4f769aa5a2 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc @@ -4,7 +4,7 @@ [.small]#xref:web/webflux/security.adoc[See equivalent in the Reactive stack]# -The https://spring.io/projects/spring-security[Spring Security] project provides support +The {spring-site-projects}/spring-security[Spring Security] project provides support for protecting web applications from malicious exploits. See the Spring Security reference documentation, including: @@ -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.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet.adoc index 173ee07e6797..5205cec16d13 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet.adoc @@ -22,7 +22,7 @@ the `DispatcherServlet`, which is auto-detected by the Servlet container ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyWebApplicationInitializer implements WebApplicationInitializer { @@ -44,7 +44,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyWebApplicationInitializer : WebApplicationInitializer { @@ -70,7 +70,7 @@ NOTE: In addition to using the ServletContext API directly, you can also extend NOTE: For programmatic use cases, a `GenericWebApplicationContext` can be used as an alternative to `AnnotationConfigWebApplicationContext`. See the -{api-spring-framework}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] +{spring-framework-api}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] javadoc for details. The following example of `web.xml` configuration registers and initializes the `DispatcherServlet`: @@ -111,7 +111,7 @@ the lifecycle of the Servlet container, Spring Boot uses Spring configuration to bootstrap itself and the embedded Servlet container. `Filter` and `Servlet` declarations are detected in Spring configuration and registered with the Servlet container. For more details, see the -https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-embedded-container[Spring Boot documentation]. +{spring-boot-docs-ref}/web/servlet.html#web.servlet.embedded-container[Spring Boot documentation]. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/config.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/config.adoc index 64fcc6e1be63..2a8950c073fe 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/config.adoc @@ -8,7 +8,7 @@ Applications can declare the infrastructure beans listed in xref:web/webmvc/mvc- that are required to process requests. The `DispatcherServlet` checks the `WebApplicationContext` for each special bean. If there are no matching bean types, it falls back on the default types listed in -{spring-framework-main-code}/spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties[`DispatcherServlet.properties`]. +{spring-framework-code}/spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties[`DispatcherServlet.properties`]. In most cases, the xref:web/webmvc/mvc-config.adoc[MVC Config] is the best starting point. It declares the required beans in either Java or XML and provides a higher-level configuration callback API to diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/container-config.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/container-config.adoc index a6a4755c0230..2ae4827d533b 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/container-config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/container-config.adoc @@ -9,7 +9,7 @@ The following example registers a `DispatcherServlet`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.WebApplicationInitializer; @@ -29,7 +29,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import org.springframework.web.WebApplicationInitializer @@ -62,7 +62,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @@ -85,7 +85,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { @@ -111,7 +111,7 @@ If you use XML-based Spring configuration, you should extend directly from ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyWebAppInitializer extends AbstractDispatcherServletInitializer { @@ -136,7 +136,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyWebAppInitializer : AbstractDispatcherServletInitializer() { @@ -165,7 +165,7 @@ following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyWebAppInitializer extends AbstractDispatcherServletInitializer { @@ -181,7 +181,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyWebAppInitializer : AbstractDispatcherServletInitializer() { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/context-hierarchy.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/context-hierarchy.adoc index 5e18f2e21eac..84f233adb498 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/context-hierarchy.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/context-hierarchy.adoc @@ -28,7 +28,7 @@ The following example configures a `WebApplicationContext` hierarchy: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @@ -51,7 +51,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/exceptionhandlers.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/exceptionhandlers.adoc index 23e8587b3bde..21e062296def 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/exceptionhandlers.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/exceptionhandlers.adoc @@ -19,7 +19,7 @@ The following table lists the available `HandlerExceptionResolver` implementatio | A mapping between exception class names and error view names. Useful for rendering error pages in a browser application. -| {api-spring-framework}/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html[`DefaultHandlerExceptionResolver`] +| {spring-framework-api}/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html[`DefaultHandlerExceptionResolver`] | Resolves exceptions raised by Spring MVC and maps them to HTTP status codes. See also alternative `ResponseEntityExceptionHandler` and xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]. @@ -78,7 +78,7 @@ or to render a JSON response, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController public class ErrorController { @@ -95,7 +95,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController class ErrorController { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc index 95aa38fbd334..f153256002e5 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc @@ -1,34 +1,29 @@ [[mvc-handlermapping-interceptor]] = Interception -All `HandlerMapping` implementations support handler interceptors that are useful when -you want to apply specific functionality to certain requests -- for example, checking for -a principal. Interceptors must implement `HandlerInterceptor` from the -`org.springframework.web.servlet` package with three methods that should provide enough -flexibility to do all kinds of pre-processing and post-processing: - -* `preHandle(..)`: Before the actual handler is run -* `postHandle(..)`: After the handler is run -* `afterCompletion(..)`: After the complete request has finished - -The `preHandle(..)` method returns a boolean value. You can use this method to break or -continue the processing of the execution chain. When this method returns `true`, the -handler execution chain continues. When it returns false, the `DispatcherServlet` -assumes the interceptor itself has taken care of requests (and, for example, rendered an -appropriate view) and does not continue executing the other interceptors and the actual -handler in the execution chain. +All `HandlerMapping` implementations support handler interception which is useful when +you want to apply functionality across requests. A `HandlerInterceptor` can implement the +following: + +* `preHandle(..)` -- callback before the actual handler is run that returns a boolean. +If the method returns `true`, execution continues; if it returns `false`, the rest of the +execution chain is bypassed and the handler is not called. +* `postHandle(..)` -- callback after the handler is run. +* `afterCompletion(..)` -- callback after the complete request has finished. + +NOTE: For `@ResponseBody` and `ResponseEntity` controller methods, the response is written +and committed within the `HandlerAdapter`, before `postHandle` is called. That means it is +too late to change the response, such as to add an extra header. You can implement +`ResponseBodyAdvice` and declare it as an +xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice] bean or configure it +directly on `RequestMappingHandlerAdapter`. See xref:web/webmvc/mvc-config/interceptors.adoc[Interceptors] in the section on MVC configuration for examples of how to configure interceptors. You can also register them directly by using setters on individual `HandlerMapping` implementations. -`postHandle` method is less useful with `@ResponseBody` and `ResponseEntity` methods for -which the response is written and committed within the `HandlerAdapter` and before -`postHandle`. That means it is too late to make any changes to the response, such as adding -an extra header. For such scenarios, you can implement `ResponseBodyAdvice` and either -declare it as an xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice] bean or configure it directly on -`RequestMappingHandlerAdapter`. - - - +WARNING: Interceptors are not ideally suited as a security layer due to the potential for +a mismatch with annotated controller path matching. Generally, we recommend using Spring +Security, or alternatively a similar approach integrated with the Servlet filter chain, +and applied as early as possible. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/logging.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/logging.adoc index 26e4193d81d0..24f0583fff44 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/logging.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/logging.adoc @@ -29,7 +29,7 @@ The following example shows how to do so by using Java configuration: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MyInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @@ -59,7 +59,7 @@ public class MyInitializer Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc index 44844ba9a06b..75dbbb1919b3 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc @@ -32,7 +32,7 @@ The following example shows how to set a `MultipartConfigElement` on the Servlet ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @@ -50,7 +50,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { @@ -75,7 +75,7 @@ This resolver variant uses your Servlet container's multipart parser as-is, potentially exposing the application to container implementation differences. By default, it will try to parse any `multipart/` content type with any HTTP method but this may not be supported across all Servlet containers. See the -{api-spring-framework}/web/multipart/support/StandardServletMultipartResolver.html[`StandardServletMultipartResolver`] +{spring-framework-api}/web/multipart/support/StandardServletMultipartResolver.html[`StandardServletMultipartResolver`] javadoc for details and configuration options. ==== 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/webmvc/mvc-servlet/viewresolver.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/viewresolver.adoc index b6728feb674f..e7e5dad266f3 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/viewresolver.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/viewresolver.adoc @@ -32,7 +32,7 @@ The following table provides more details on the `ViewResolver` hierarchy: | Convenient subclass of `UrlBasedViewResolver` that supports `InternalResourceView` (in effect, Servlets and JSPs) and subclasses such as `JstlView`. You can specify the view class for all views generated by this resolver by using `setViewClass(..)`. - See the {api-spring-framework}/web/reactive/result/view/UrlBasedViewResolver.html[`UrlBasedViewResolver`] + See the {spring-framework-api}/web/reactive/result/view/UrlBasedViewResolver.html[`UrlBasedViewResolver`] javadoc for details. | `FreeMarkerViewResolver` @@ -47,7 +47,7 @@ The following table provides more details on the `ViewResolver` hierarchy: | Implementation of the `ViewResolver` interface that interprets a view name as a bean name in the current application context. This is a very flexible variant which allows for mixing and matching different view types based on distinct view names. - Each such `View` can be defined as a bean e.g. in XML or in configuration classes. + Each such `View` can be defined as a bean, for example, in XML or in configuration classes. |=== @@ -103,7 +103,7 @@ Servlet/JSP engine. Note that you may also chain multiple view resolvers, instea == Content Negotiation [.small]#xref:web/webflux/dispatcher-handler.adoc#webflux-multiple-representations[See equivalent in the Reactive stack]# -{api-spring-framework}/web/servlet/view/ContentNegotiatingViewResolver.html[`ContentNegotiatingViewResolver`] +{spring-framework-api}/web/servlet/view/ContentNegotiatingViewResolver.html[`ContentNegotiatingViewResolver`] does not resolve views itself but rather delegates to other view resolvers and selects the view that resembles the representation requested by the client. The representation can be determined from the `Accept` header or from a diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-uri-building.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-uri-building.adoc index 3e18dae861ad..4aba9d6ff6c2 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-uri-building.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-uri-building.adoc @@ -19,7 +19,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpServletRequest request = ... @@ -32,7 +32,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val request: HttpServletRequest = ... @@ -50,7 +50,7 @@ You can create URIs relative to the context path, as the following example shows ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpServletRequest request = ... @@ -64,7 +64,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val request: HttpServletRequest = ... @@ -84,7 +84,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- HttpServletRequest request = ... @@ -98,7 +98,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val request: HttpServletRequest = ... @@ -128,7 +128,7 @@ the following MVC controller allows for link creation: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/hotels/{hotel}") @@ -143,7 +143,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/hotels/{hotel}") @@ -163,7 +163,7 @@ You can prepare a link by referring to the method by name, as the following exam ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- UriComponents uriComponents = MvcUriComponentsBuilder .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42); @@ -173,7 +173,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val uriComponents = MvcUriComponentsBuilder .fromMethodName(BookingController::class.java, "getBooking", 21).buildAndExpand(42) @@ -197,7 +197,7 @@ akin to mock testing through proxies to avoid referring to the controller method ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- UriComponents uriComponents = MvcUriComponentsBuilder .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42); @@ -207,7 +207,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val uriComponents = MvcUriComponentsBuilder .fromMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42) @@ -240,7 +240,7 @@ following listing uses `withMethodCall`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en"); MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base); @@ -251,7 +251,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en") val builder = MvcUriComponentsBuilder.relativeTo(base) @@ -280,7 +280,7 @@ Consider the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RequestMapping("/people/{id}/addresses") public class PersonAddressController { @@ -292,7 +292,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RequestMapping("/people/{id}/addresses") class PersonAddressController { diff --git a/framework-docs/modules/ROOT/pages/web/websocket.adoc b/framework-docs/modules/ROOT/pages/web/websocket.adoc index 726c9c2de3ed..f917e7e09390 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket.adoc @@ -1,6 +1,7 @@ [[websocket]] = WebSockets :page-section-summary-toc: 1 + [.small]#xref:web/webflux-websocket.adoc[See equivalent in the Reactive stack]# This part of the reference documentation covers support for Servlet stack, WebSocket diff --git a/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc b/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc index f565c2250a10..c79fd0a70f86 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc @@ -23,20 +23,20 @@ change application code. SockJS consists of: -* The https://github.com/sockjs/sockjs-protocol[SockJS protocol] +* The {sockjs-protocol}[SockJS protocol] defined in the form of executable -https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html[narrated tests]. -* The https://github.com/sockjs/sockjs-client/[SockJS JavaScript client] -- a client library for use in browsers. +{sockjs-protocol-site}/sockjs-protocol-0.3.3.html[narrated tests]. +* The {sockjs-client}[SockJS JavaScript client] -- a client library for use in browsers. * SockJS server implementations, including one in the Spring Framework `spring-websocket` module. * A SockJS Java client in the `spring-websocket` module (since version 4.1). SockJS is designed for use in browsers. It uses a variety of techniques to support a wide range of browser versions. For the full list of SockJS transport types and browsers, see the -https://github.com/sockjs/sockjs-client/[SockJS client] page. Transports +{sockjs-client}[SockJS client] page. Transports fall in three general categories: WebSocket, HTTP Streaming, and HTTP Long Polling. For an overview of these categories, see -https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post]. +{spring-site-blog}/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post]. The SockJS client begins by sending `GET /info` to obtain basic information from the server. After that, it must decide what transport @@ -82,61 +82,21 @@ https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html[narrated tes [[websocket-fallback-sockjs-enable]] == Enabling SockJS -You can enable SockJS through Java configuration, as the following example shows: +You can enable SockJS through configuration, as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocket - public class WebSocketConfig implements WebSocketConfigurer { - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(myHandler(), "/myHandler").withSockJS(); - } - - @Bean - public WebSocketHandler myHandler() { - return new MyHandler(); - } - - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - - ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] The preceding example is for use in Spring MVC applications and should be included in the configuration of a xref:web/webmvc/mvc-servlet.adoc[`DispatcherServlet`]. However, Spring's WebSocket and SockJS support does not depend on Spring MVC. It is relatively simple to integrate into other HTTP serving environments with the help of -{api-spring-framework}/web/socket/sockjs/support/SockJsHttpRequestHandler.html[`SockJsHttpRequestHandler`]. +{spring-framework-api}/web/socket/sockjs/support/SockJsHttpRequestHandler.html[`SockJsHttpRequestHandler`]. On the browser side, applications can use the -https://github.com/sockjs/sockjs-client/[`sockjs-client`] (version 1.0.x). It +{sockjs-client}[`sockjs-client`] (version 1.0.x). It emulates the W3C WebSocket API and communicates with the server to select the best transport option, depending on the browser in which it runs. See the -https://github.com/sockjs/sockjs-client/[sockjs-client] page and the list of +{sockjs-client}[sockjs-client] page and the list of transport types supported by browser. The client also provides several configuration options -- for example, to specify which transports to include. @@ -183,7 +143,7 @@ but can be configured to do so. In the future, it may set it by default. See {docs-spring-security}/features/exploits/headers.html#headers-default[Default Security Headers] of the Spring Security documentation for details on how to configure the setting of the `X-Frame-Options` header. You can also see -https://github.com/spring-projects/spring-security/issues/2718[gh-2718] +{spring-github-org}/spring-security/issues/2718[gh-2718] for additional background. ==== @@ -219,7 +179,7 @@ The XML namespace provides a similar option through the `` ele NOTE: During initial development, do enable the SockJS client `devel` mode that prevents the browser from caching SockJS requests (like the iframe) that would otherwise be cached. For details on how to enable it see the -https://github.com/sockjs/sockjs-client/[SockJS client] page. +{sockjs-client}[SockJS client] page. @@ -231,14 +191,14 @@ from concluding that a connection is hung. The Spring SockJS configuration has a called `heartbeatTime` that you can use to customize the frequency. By default, a heartbeat is sent after 25 seconds, assuming no other messages were sent on that connection. This 25-second value is in line with the following -https://tools.ietf.org/html/rfc6202[IETF recommendation] for public Internet applications. +{rfc-site}/rfc6202[IETF recommendation] for public Internet applications. NOTE: When using STOMP over WebSocket and SockJS, if the STOMP client and server negotiate heartbeats to be exchanged, the SockJS heartbeats are disabled. The Spring SockJS support also lets you configure the `TaskScheduler` to schedule heartbeats tasks. The task scheduler is backed by a thread pool, -with default settings based on the number of available processors. Your +with default settings based on the number of available processors. You should consider customizing the settings according to your specific needs. @@ -248,7 +208,7 @@ should consider customizing the settings according to your specific needs. HTTP streaming and HTTP long polling SockJS transports require a connection to remain open longer than usual. For an overview of these techniques, see -https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post]. +{spring-site-blog}/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post]. In Servlet containers, this is done through Servlet 3 asynchronous support that allows exiting the Servlet container thread, processing a request, and continuing diff --git a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc index 5fc4da352e64..1e2fcd1595eb 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc @@ -16,81 +16,24 @@ Creating a WebSocket server is as simple as implementing `WebSocketHandler` or, likely, extending either `TextWebSocketHandler` or `BinaryWebSocketHandler`. The following example uses `TextWebSocketHandler`: -[source,java,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.web.socket.WebSocketHandler; - import org.springframework.web.socket.WebSocketSession; - import org.springframework.web.socket.TextMessage; - - public class MyHandler extends TextWebSocketHandler { - - @Override - public void handleTextMessage(WebSocketSession session, TextMessage message) { - // ... - } - - } ----- +include-code::./MyHandler[tag=snippet,indent=0] -There is dedicated WebSocket Java configuration and XML namespace support for mapping the preceding +There is dedicated WebSocket programmatic configuration and XML namespace support for mapping the preceding WebSocket handler to a specific URL, as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.web.socket.config.annotation.EnableWebSocket; - import org.springframework.web.socket.config.annotation.WebSocketConfigurer; - import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; - - @Configuration - @EnableWebSocket - public class WebSocketConfig implements WebSocketConfigurer { - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(myHandler(), "/myHandler"); - } - - @Bean - public WebSocketHandler myHandler() { - return new MyHandler(); - } - - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] The preceding example is for use in Spring MVC applications and should be included in the configuration of a xref:web/webmvc/mvc-servlet.adoc[`DispatcherServlet`]. However, Spring's WebSocket support does not depend on Spring MVC. It is relatively simple to integrate a `WebSocketHandler` into other HTTP-serving environments with the help of -{api-spring-framework}/web/socket/server/support/WebSocketHttpRequestHandler.html[`WebSocketHttpRequestHandler`]. +{spring-framework-api}/web/socket/server/support/WebSocketHttpRequestHandler.html[`WebSocketHttpRequestHandler`]. -When using the `WebSocketHandler` API directly vs indirectly, e.g. through the +When using the `WebSocketHandler` API directly vs indirectly, for example, through the xref:web/websocket/stomp.adoc[STOMP] messaging, the application must synchronize the sending of messages since the underlying standard WebSocket session (JSR-356) does not allow concurrent sending. One option is to wrap the `WebSocketSession` with -{api-spring-framework}/web/socket/handler/ConcurrentWebSocketSessionDecorator.html[`ConcurrentWebSocketSessionDecorator`]. +{spring-framework-api}/web/socket/handler/ConcurrentWebSocketSessionDecorator.html[`ConcurrentWebSocketSessionDecorator`]. @@ -104,45 +47,7 @@ You can use such an interceptor to preclude the handshake or to make any attribu available to the `WebSocketSession`. The following example uses a built-in interceptor to pass HTTP session attributes to the WebSocket session: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocket - public class WebSocketConfig implements WebSocketConfigurer { - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(new MyHandler(), "/myHandler") - .addInterceptors(new HttpSessionHandshakeInterceptor()); - } - - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - - - - ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] A more advanced option is to extend the `DefaultHandshakeHandler` that performs the steps of the WebSocket handshake, including validating the client origin, @@ -180,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 @@ -229,81 +131,28 @@ Java initialization API. The following example shows how to do so: [[websocket-server-runtime-configuration]] -== Server Configuration +== Configuring the Server [.small]#xref:web/webflux-websocket.adoc#webflux-websocket-server-config[See equivalent in the Reactive stack]# -Each underlying WebSocket engine exposes configuration properties that control -runtime characteristics, such as the size of message buffer sizes, idle timeout, -and others. - -For Tomcat, WildFly, and GlassFish, you can add a `ServletServerContainerFactoryBean` to your -WebSocket Java config, as the following example shows: +You can configure of the underlying WebSocket server such as input message buffer size, +idle timeout, and more. -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocket - public class WebSocketConfig implements WebSocketConfigurer { - - @Bean - public ServletServerContainerFactoryBean createWebSocketContainer() { - ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); - container.setMaxTextMessageBufferSize(8192); - container.setMaxBinaryMessageBufferSize(8192); - return container; - } - - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - +For Jakarta WebSocket servers, you can add a `ServletServerContainerFactoryBean` to your +configuration. For example: - ----- - -NOTE: For client-side WebSocket configuration, you should use `WebSocketContainerFactoryBean` -(XML) or `ContainerProvider.getWebSocketContainer()` (Java configuration). +include-code::./WebSocketConfiguration[tag=snippet,indent=0] -For Jetty, you need to supply a `Consumer` callback to configure the WebSocket server. For example: +NOTE: For client Jakarta WebSocket configuration, use +ContainerProvider.getWebSocketContainer() in programmatic configuration, or +`WebSocketContainerFactoryBean` in XML. -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocket - public class WebSocketConfig implements WebSocketConfigurer { +For Jetty, you can supply a callback to configure the WebSocket server: - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - - JettyRequestUpgradeStrategy upgradeStrategy = new JettyRequestUpgradeStrategy(); - upgradeStrategy.addWebSocketConfigurer(configurable -> { - policy.setInputBufferSize(8192); - policy.setIdleTimeout(600000); - }); - - registry.addHandler(echoWebSocketHandler(), - "/echo").setHandshakeHandler(new DefaultHandshakeHandler(upgradeStrategy)); - } - - } ----- +include-code::./JettyWebSocketConfiguration[tag=snippet,indent=0] +TIP: When using STOMP over WebSocket, you will also need to configure +xref:web/websocket/stomp/server-config.adoc[STOMP WebSocket transport] +properties. @@ -315,7 +164,7 @@ As of Spring Framework 4.1.5, the default behavior for WebSocket and SockJS is t only same-origin requests. It is also possible to allow all or a specified list of origins. This check is mostly designed for browser clients. Nothing prevents other types of clients from modifying the `Origin` header value (see -https://tools.ietf.org/html/rfc6454[RFC 6454: The Web Origin Concept] for more details). +{rfc-site}/rfc6454[RFC 6454: The Web Origin Concept] for more details). The three possible behaviors are: @@ -332,51 +181,4 @@ The three possible behaviors are: You can configure WebSocket and SockJS allowed origins, as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.web.socket.config.annotation.EnableWebSocket; - import org.springframework.web.socket.config.annotation.WebSocketConfigurer; - import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; - - @Configuration - @EnableWebSocket - public class WebSocketConfig implements WebSocketConfigurer { - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com"); - } - - @Bean - public WebSocketHandler myHandler() { - return new MyHandler(); - } - - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - ----- - - - - +include-code::./WebSocketConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication-token-based.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication-token-based.adoc index 3ab57f8c878e..b65811e74b8a 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication-token-based.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication-token-based.adoc @@ -1,7 +1,7 @@ [[websocket-stomp-authentication-token-based]] = Token Authentication -https://github.com/spring-projects/spring-security-oauth[Spring Security OAuth] +{spring-github-org}/spring-security-oauth[Spring Security OAuth] provides support for token based security, including JSON Web Token (JWT). You can use this as the authentication mechanism in Web applications, including STOMP over WebSocket interactions, as described in the previous @@ -11,13 +11,13 @@ At the same time, cookie-based sessions are not always the best fit (for example in applications that do not maintain a server-side session or in mobile applications where it is common to use headers for authentication). -The https://tools.ietf.org/html/rfc6455#section-10.5[WebSocket protocol, RFC 6455] +The {rfc-site}/rfc6455#section-10.5[WebSocket protocol, RFC 6455] "doesn't prescribe any particular way that servers can authenticate clients during the WebSocket handshake." In practice, however, browser clients can use only standard authentication headers (that is, basic HTTP authentication) or cookies and cannot (for example) provide custom headers. Likewise, the SockJS JavaScript client does not provide a way to send HTTP headers with SockJS transport requests. See -https://github.com/sockjs/sockjs-client/issues/196[sockjs-client issue 196]. +{sockjs-client}/issues/196[sockjs-client issue 196]. Instead, it does allow sending query parameters that you can use to send a token, but that has its own drawbacks (for example, the token may be inadvertently logged with the URL in server logs). @@ -38,31 +38,9 @@ The next example uses server-side configuration to register a custom authenticat interceptor. Note that an interceptor needs only to authenticate and set the user header on the CONNECT `Message`. Spring notes and saves the authenticated user and associate it with subsequent STOMP messages on the same session. The following -example shows how register a custom authentication interceptor: +example shows how to register a custom authentication interceptor: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class MyConfig implements WebSocketMessageBrokerConfigurer { - - @Override - public void configureClientInboundChannel(ChannelRegistration registration) { - registration.interceptors(new ChannelInterceptor() { - @Override - public Message preSend(Message message, MessageChannel channel) { - StompHeaderAccessor accessor = - MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); - if (StompCommand.CONNECT.equals(accessor.getCommand())) { - Authentication user = ... ; // access authentication header(s) - accessor.setUser(user); - } - return message; - } - }); - } - } ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] Also, note that, when you use Spring Security's authorization for messages, at present, you need to ensure that the authentication `ChannelInterceptor` config is ordered diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc index 5866ec3dc521..95af447e597e 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc @@ -6,7 +6,7 @@ Spring Security provides {docs-spring-security}/servlet/integrations/websocket.html#websocket-authorization[WebSocket sub-protocol authorization] that uses a `ChannelInterceptor` to authorize messages based on the user header in them. Also, Spring Session provides -https://docs.spring.io/spring-session/reference/web-socket.html[WebSocket integration] +{docs-spring-session}/web-socket.html[WebSocket integration] that ensures the user's HTTP session does not expire while the WebSocket session is still active. diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc index 1eae09021206..ba205223a86b 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc @@ -105,5 +105,20 @@ it handle ERROR frames in addition to the `handleException` callback for exceptions from the handling of messages and `handleTransportError` for transport-level errors including `ConnectionLostException`. +You can use the `inboundMessageSizeLimit` and `outboundMessageSizeLimit` properties of +`WebSocketStompClient` to limit the maximum size of inbound and outbound WebSocket +messages. When an outbound STOMP message exceeds the limit, it is split into partial frames, +which the receiver would have to reassemble. By default, there is no size limit for outbound +messages. When an inbound STOMP message size exceeds the configured limit, a +`StompConversionException` is thrown. The default size limit for inbound messages is `64KB`. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + WebSocketClient webSocketClient = new StandardWebSocketClient(); + WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); + stompClient.setInboundMessageSizeLimit(64 * 1024); // 64KB + stompClient.setOutboundMessageSizeLimit(64 * 1024); // 64KB +---- + diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc index aa0017e093dc..cc5df948025f 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc @@ -62,50 +62,15 @@ documentation of the XML schema for important additional details. The following example shows a possible configuration: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - @Override - public void configureWebSocketTransport(WebSocketTransportRegistration registration) { - registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024); - } - - // ... - - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] You can also use the WebSocket transport configuration shown earlier to configure the maximum allowed size for incoming STOMP messages. In theory, a WebSocket message can be almost unlimited in size. In practice, WebSocket servers impose limits -- for example, 8K on Tomcat and 64K on Jetty. For this reason, STOMP clients -(such as the JavaScript https://github.com/JSteunou/webstomp-client[webstomp-client] -and others) split larger STOMP messages at 16K boundaries and send them as multiple -WebSocket messages, which requires the server to buffer and re-assemble. +such as https://github.com/stomp-js/stompjs[`stomp-js/stompjs`] and others split larger +STOMP messages at 16K boundaries and send them as multiple WebSocket messages, +which requires the server to buffer and re-assemble. Spring's STOMP-over-WebSocket support does this ,so applications can configure the maximum size for STOMP messages irrespective of WebSocket server-specific message @@ -115,42 +80,7 @@ minimum. The following example shows one possible configuration: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - @Override - public void configureWebSocketTransport(WebSocketTransportRegistration registration) { - registration.setMessageSizeLimit(128 * 1024); - } - - // ... - - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - ----- +include-code::./MessageSizeLimitWebSocketConfiguration[tag=snippet,indent=0] An important point about scaling involves using multiple application instances. Currently, you cannot do that with the simple broker. diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/destination-separator.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/destination-separator.adoc index 866b6d81e07a..0c81e2c2b861 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/destination-separator.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/destination-separator.adoc @@ -6,65 +6,14 @@ When messages are routed to `@MessageMapping` methods, they are matched with This is a good convention in web applications and similar to HTTP URLs. However, if you are more used to messaging conventions, you can switch to using dot (`.`) as the separator. -The following example shows how to do so in Java configuration: +The following example shows how to do so: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - // ... - - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.setPathMatcher(new AntPathMatcher(".")); - registry.enableStompBrokerRelay("/queue", "/topic"); - registry.setApplicationDestinationPrefixes("/app"); - } - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - - - - ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] After that, a controller can use a dot (`.`) as the separator in `@MessageMapping` methods, as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - @MessageMapping("red") - public class RedController { - - @MessageMapping("blue.{green}") - public void handleGreen(@DestinationVariable String green) { - // ... - } - } ----- +include-code::./RedController[tag=snippet,indent=0] The client can now send a message to `/app/red.blue.green123`. 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 8ff62f4c6752..4301ba970868 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc @@ -5,56 +5,7 @@ STOMP over WebSocket support is available in the `spring-messaging` and `spring-websocket` modules. Once you have those dependencies, you can expose a STOMP endpoint over WebSocket, as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; - import org.springframework.web.socket.config.annotation.StompEndpointRegistry; - - @Configuration - @EnableWebSocketMessageBroker - public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/portfolio"); // <1> - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry config) { - config.setApplicationDestinationPrefixes("/app"); // <2> - config.enableSimpleBroker("/topic", "/queue"); // <3> - } - } ----- - -<1> `/portfolio` is the HTTP URL for the endpoint to which a WebSocket (or SockJS) -client needs to connect for the WebSocket handshake. -<2> STOMP messages whose destination header begins with `/app` are routed to -`@MessageMapping` methods in `@Controller` classes. -<3> Use the built-in message broker for subscriptions and broadcasting and -route messages whose destination header begins with `/topic` or `/queue` to the broker. - - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] NOTE: For the built-in simple broker, the `/topic` and `/queue` prefixes do not have any special meaning. They are merely a convention to differentiate between pub-sub versus point-to-point @@ -91,7 +42,7 @@ and xref:web/websocket/stomp/authentication.adoc[Authentication] for more inform For more example code see: -* https://spring.io/guides/gs/messaging-stomp-websocket/[Using WebSocket to build an +* {spring-site-guides}/gs/messaging-stomp-websocket/[Using WebSocket to build an 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/pages/web/websocket/stomp/handle-broker-relay-configure.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-broker-relay-configure.adoc index 853732b6000d..f13d532367fe 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-broker-relay-configure.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-broker-relay-configure.adoc @@ -36,27 +36,7 @@ connectivity is lost, to the same host and port. If you wish to supply multiple on each attempt to connect, you can configure a supplier of addresses, instead of a fixed host and port. The following example shows how to do that: -[source,java,indent=0,subs="verbatim,quotes"] ----- -@Configuration -@EnableWebSocketMessageBroker -public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { - - // ... - - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient()); - registry.setApplicationDestinationPrefixes("/app"); - } - - private ReactorNettyTcpClient createTcpClient() { - return new ReactorNettyTcpClient<>( - client -> client.addressSupplier(() -> ... ), - new StompReactorNettyCodec()); - } -} ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] You can also configure the STOMP broker relay with a `virtualHost` property. The value of this property is set as the `host` header of every `CONNECT` frame diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-broker-relay.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-broker-relay.adoc index 8db28f6dd504..fd0ddcec2267 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-broker-relay.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-broker-relay.adoc @@ -15,51 +15,10 @@ and run it with STOMP support enabled. Then you can enable the STOMP broker rela The following example configuration enables a full-featured broker: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/portfolio").withSockJS(); - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableStompBrokerRelay("/topic", "/queue"); - registry.setApplicationDestinationPrefixes("/app"); - } - - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - - ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] The STOMP broker relay in the preceding configuration is a Spring -{api-spring-framework}/messaging/MessageHandler.html[`MessageHandler`] +{spring-framework-api}/messaging/MessageHandler.html[`MessageHandler`] that handles messages by forwarding them to an external message broker. To do so, it establishes TCP connections to the broker, forwards all messages to it, and then forwards all messages received from the broker to clients through their diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-simple-broker.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-simple-broker.adoc index 5be3fdb6ae22..15efa72b1015 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-simple-broker.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-simple-broker.adoc @@ -13,33 +13,8 @@ If configured with a task scheduler, the simple broker supports https://stomp.github.io/stomp-specification-1.2.html#Heart-beating[STOMP heartbeats]. To configure a scheduler, you can declare your own `TaskScheduler` bean and set it through the `MessageBrokerRegistry`. Alternatively, you can use the one that is automatically -declared in the built-in WebSocket configuration, however, you'll' need `@Lazy` to avoid +declared in the built-in WebSocket configuration, however, you'll need `@Lazy` to avoid a cycle between the built-in WebSocket configuration and your `WebSocketMessageBrokerConfigurer`. For example: -[source,java,indent=0,subs="verbatim,quotes"] ----- -@Configuration -@EnableWebSocketMessageBroker -public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - private TaskScheduler messageBrokerTaskScheduler; - - @Autowired - public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) { - this.messageBrokerTaskScheduler = taskScheduler; - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/queue/", "/topic/") - .setHeartbeatValue(new long[] {10000, 20000}) - .setTaskScheduler(this.messageBrokerTaskScheduler); - - // ... - } -} ----- - - - +include-code::./WebSocketConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/interceptors.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/interceptors.adoc index 7a21a1b8d40d..9bdab9835166 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/interceptors.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/interceptors.adoc @@ -6,35 +6,12 @@ of a STOMP connection but not for every client message. Applications can also re `ChannelInterceptor` to intercept any message and in any part of the processing chain. The following example shows how to intercept inbound messages from clients: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - @Override - public void configureClientInboundChannel(ChannelRegistration registration) { - registration.interceptors(new MyChannelInterceptor()); - } - } ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] A custom `ChannelInterceptor` can use `StompHeaderAccessor` or `SimpMessageHeaderAccessor` to access information about the message, as the following example shows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - public class MyChannelInterceptor implements ChannelInterceptor { - - @Override - public Message preSend(Message message, MessageChannel channel) { - StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); - StompCommand command = accessor.getStompCommand(); - // ... - return message; - } - } ----- +include-code::./MyChannelInterceptor[tag=snippet,indent=0] Applications can also implement `ExecutorChannelInterceptor`, which is a sub-interface of `ChannelInterceptor` with callbacks in the thread in which the messages are handled. diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/message-flow.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/message-flow.adoc index 275ab0a27bd0..aee0cd9adc8b 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/message-flow.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/message-flow.adoc @@ -7,18 +7,18 @@ connected clients. This section describes the flow of messages on the server sid The `spring-messaging` module contains foundational support for messaging applications that originated in https://spring.io/spring-integration[Spring Integration] and was later extracted and incorporated into the Spring Framework for broader use across many -https://spring.io/projects[Spring projects] and application scenarios. +{spring-site-projects}[Spring projects] and application scenarios. The following list briefly describes a few of the available messaging abstractions: -* {api-spring-framework}/messaging/Message.html[Message]: +* {spring-framework-api}/messaging/Message.html[Message]: Simple representation for a message, including headers and payload. -* {api-spring-framework}/messaging/MessageHandler.html[MessageHandler]: +* {spring-framework-api}/messaging/MessageHandler.html[MessageHandler]: Contract for handling a message. -* {api-spring-framework}/messaging/MessageChannel.html[MessageChannel]: +* {spring-framework-api}/messaging/MessageChannel.html[MessageChannel]: Contract for sending a message that enables loose coupling between producers and consumers. -* {api-spring-framework}/messaging/SubscribableChannel.html[SubscribableChannel]: +* {spring-framework-api}/messaging/SubscribableChannel.html[SubscribableChannel]: `MessageChannel` with `MessageHandler` subscribers. -* {api-spring-framework}/messaging/support/ExecutorSubscribableChannel.html[ExecutorSubscribableChannel]: +* {spring-framework-api}/messaging/support/ExecutorSubscribableChannel.html[ExecutorSubscribableChannel]: `SubscribableChannel` that uses an `Executor` for delivering messages. Both the Java configuration (that is, `@EnableWebSocketMessageBroker`) and the XML namespace configuration @@ -60,33 +60,9 @@ to broadcast to subscribed clients. We can trace the flow through a simple example. Consider the following example, which sets up a server: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/portfolio"); - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.setApplicationDestinationPrefixes("/app"); - registry.enableSimpleBroker("/topic"); - } - } - - @Controller - public class GreetingController { - - @MessageMapping("/greeting") - public String handle(String greeting) { - return "[" + getTimestamp() + ": " + greeting; - } - } ----- +include-code::./WebSocketConfiguration[tag=snippet,indent=0] + +include-code::./GreetingController[tag=snippet,indent=0] The preceding example supports the following flow: diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/ordered-messages.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/ordered-messages.adoc index 89e60da926c2..d56552286f2d 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/ordered-messages.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/ordered-messages.adoc @@ -8,40 +8,7 @@ not match the exact order of publication. To enable ordered publishing, set the `setPreservePublishOrder` flag as follows: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class MyConfig implements WebSocketMessageBrokerConfigurer { - - @Override - protected void configureMessageBroker(MessageBrokerRegistry registry) { - // ... - registry.setPreservePublishOrder(true); - } - - } ----- - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - ----- +include-code::./PublishOrderWebSocketConfiguration[tag=snippet,indent=0] When the flag is set, messages within the same client session are published to the `clientOutboundChannel` one at a time, so that the order of publication is guaranteed. @@ -52,17 +19,6 @@ from where they are handled according to their destination prefix. As the channe a `ThreadPoolExecutor`, messages are processed in different threads, and the resulting sequence of handling may not match the exact order in which they were received. -To enable ordered publishing, set the `setPreserveReceiveOrder` flag as follows: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class MyConfig implements WebSocketMessageBrokerConfigurer { +To enable ordered receiving, set the `setPreserveReceiveOrder` flag as follows: - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.setPreserveReceiveOrder(true); - } - } ----- +include-code::./ReceiveOrderWebSocketConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc index 92538177177f..5903c07051bb 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc @@ -1,33 +1,18 @@ [[websocket-stomp-server-config]] -= WebSocket Server += WebSocket Transport -To configure the underlying WebSocket server, the information in -xref:web/websocket/server.adoc#websocket-server-runtime-configuration[Server Configuration] applies. For Jetty, however you need to set -the `HandshakeHandler` and `WebSocketPolicy` through the `StompEndpointRegistry`: +This section explains how to configure the underlying WebSocket server transport. -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { +For Jakarta WebSocket servers, add a `ServletServerContainerFactoryBean` to your +configuration. For examples, see +xref:web/websocket/server.adoc#websocket-server-runtime-configuration[Configuring the Server] +under the WebSocket section. - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler()); - } - - @Bean - public DefaultHandshakeHandler handshakeHandler() { - - WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); - policy.setInputBufferSize(8192); - policy.setIdleTimeout(600000); - - return new DefaultHandshakeHandler( - new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy))); - } - } ----- +For Jetty WebSocket servers, customize the `JettyRequestUpgradeStrategy` as follows: +include-code::./JettyWebSocketConfiguration[tag=snippet,indent=0] +In addition to WebSocket server properties, there are also STOMP WebSocket transport properties +to customize as follows: +include-code::./WebSocketConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc index 37cbba42d8bf..a56c0d1893ed 100644 --- a/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc +++ b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc @@ -2,7 +2,7 @@ As a request goes through proxies such as load balancers the host, port, and scheme may change, and that makes it a challenge to create links that point to the correct host, port, and scheme from a client perspective. -https://tools.ietf.org/html/rfc7239[RFC 7239] defines the `Forwarded` HTTP header +{rfc-site}/rfc7239[RFC 7239] defines the `Forwarded` HTTP header that proxies can use to provide information about the original request. @@ -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 (e.g. https / https) +is a de-facto standard header that is used to communicate the original protocol (for example, https / https) 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`. @@ -48,7 +48,7 @@ a proxy which forwards the request to `http://localhost:8080/resource`, then a h ==== X-Forwarded-Ssl While not standard, `X-Forwarded-Ssl: (on|off)` is a de-facto standard header that is used to communicate the -original protocol (e.g. https / https) to a downstream server. For example, if a request of +original protocol (for example, https / https) 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-Ssl: on` to inform the server that the original protocol was `https`. @@ -103,7 +103,7 @@ applications on the same server. However, this should not be visible in URL path the public API where applications may use different subdomains that provides benefits such as: -* Added security, e.g. same origin policy +* Added security, for example, same origin policy * Independent scaling of applications (different domain points to different IP address) ==== diff --git a/framework-docs/modules/ROOT/partials/web/web-data-binding-model-design.adoc b/framework-docs/modules/ROOT/partials/web/web-data-binding-model-design.adoc index 90ee5468e338..74fe12d51bb9 100644 --- a/framework-docs/modules/ROOT/partials/web/web-data-binding-model-design.adoc +++ b/framework-docs/modules/ROOT/partials/web/web-data-binding-model-design.adoc @@ -45,7 +45,7 @@ input is ignored. This is in contrast to property binding which by default binds request parameter for which there is a matching property. If neither a dedicated model object nor constructor binding is sufficient, and you must -use property binding, we strongy recommend registering `allowedFields` patterns (case +use property binding, we strongly recommend registering `allowedFields` patterns (case sensitive) on `WebDataBinder` in order to prevent unexpected properties from being set. For example: diff --git a/framework-docs/modules/ROOT/partials/web/web-uris.adoc b/framework-docs/modules/ROOT/partials/web/web-uris.adoc index d6e94cec95dc..8ba08ac1cbdd 100644 --- a/framework-docs/modules/ROOT/partials/web/web-uris.adoc +++ b/framework-docs/modules/ROOT/partials/web/web-uris.adoc @@ -8,7 +8,7 @@ ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- UriComponents uriComponents = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}") // <1> @@ -26,7 +26,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val uriComponents = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}") // <1> @@ -50,7 +50,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- URI uri = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}") @@ -62,7 +62,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val uri = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}") @@ -80,7 +80,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- URI uri = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}") @@ -90,7 +90,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val uri = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}") @@ -105,7 +105,7 @@ You can shorten it further still with a full URI template, as the following exam ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- URI uri = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}?q={q}") @@ -114,7 +114,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val uri = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}?q={q}") @@ -144,7 +144,7 @@ The following example shows how to configure a `RestTemplate`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; @@ -158,7 +158,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode @@ -177,7 +177,7 @@ The following example configures a `WebClient`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; @@ -190,7 +190,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode @@ -210,7 +210,7 @@ that holds configuration and preferences, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String baseUrl = "https://example.com"; DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl); @@ -222,7 +222,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val baseUrl = "https://example.com" val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl) @@ -234,15 +234,43 @@ Kotlin:: ====== +[[uri-parsing]] += URI Parsing +[.small]#Spring MVC and Spring WebFlux# + +`UriComponentsBuilder` supports two URI parser types: + +1. RFC parser -- this parser type expects URI strings to conform to RFC 3986 syntax, +and treats deviations from the syntax as illegal. + +2. WhatWG parser -- this parser is based on the +https://github.com/web-platform-tests/wpt/tree/master/url[URL parsing algorithm] in the +https://url.spec.whatwg.org[WhatWG URL Living standard]. It provides lenient handling of +a wide range of cases of unexpected input. Browsers implement this in order to handle +leniently user typed URL's. For more details, see the URL Living Standard and URL parsing +https://github.com/web-platform-tests/wpt/tree/master/url[test cases]. + +By default, `RestClient`, `WebClient`, and `RestTemplate` use the RFC parser type, and +expect applications to provide with URL templates that conform to RFC syntax. To change +that you can customize the `UriBuilderFactory` on any of the clients. + +Applications and frameworks may further rely on `UriComponentsBuilder` for their own needs +to parse user provided URL's in order to inspect and possibly validated URI components +such as the scheme, host, port, path, and query. Such components can decide to use the +WhatWG parser type in order to handle URL's more leniently, and to align with the way +browsers parse URI's, in case of a redirect to the input URL or if it is included in a +response to a browser. + + [[uri-encoding]] = URI Encoding [.small]#Spring MVC and Spring WebFlux# `UriComponentsBuilder` exposes encoding options at two levels: -* {api-spring-framework}/web/util/UriComponentsBuilder.html#encode--[UriComponentsBuilder#encode()]: +* {spring-framework-api}/web/util/UriComponentsBuilder.html#encode--[UriComponentsBuilder#encode()]: Pre-encodes the URI template first and then strictly encodes URI variables when expanded. -* {api-spring-framework}/web/util/UriComponents.html#encode--[UriComponents#encode()]: +* {spring-framework-api}/web/util/UriComponents.html#encode--[UriComponents#encode()]: Encodes URI components _after_ URI variables are expanded. Both options replace non-ASCII and illegal characters with escaped octets. However, the first option @@ -264,7 +292,7 @@ The following example uses the first option: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}") .queryParam("q", "{q}") @@ -277,7 +305,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val uri = UriComponentsBuilder.fromPath("/hotel list/{city}") .queryParam("q", "{q}") @@ -296,7 +324,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}") .queryParam("q", "{q}") @@ -305,7 +333,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val uri = UriComponentsBuilder.fromPath("/hotel list/{city}") .queryParam("q", "{q}") @@ -319,7 +347,7 @@ You can shorten it further still with a full URI template, as the following exam ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}") .build("New York", "foo+bar"); @@ -327,7 +355,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}") .build("New York", "foo+bar") @@ -342,7 +370,7 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String baseUrl = "https://example.com"; DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl) @@ -358,7 +386,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val baseUrl = "https://example.com" val factory = DefaultUriBuilderFactory(baseUrl).apply { @@ -389,7 +417,7 @@ template. encode URI component value _after_ URI variables are expanded. * `NONE`: No encoding is applied. -The `RestTemplate` is set to `EncodingMode.URI_COMPONENT` for historic +The `RestTemplate` is set to `EncodingMode.URI_COMPONENT` for historical reasons and for backwards compatibility. The `WebClient` relies on the default value in `DefaultUriBuilderFactory`, which was changed from `EncodingMode.URI_COMPONENT` in 5.0.x to `EncodingMode.TEMPLATE_AND_VALUES` in 5.1. diff --git a/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc b/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc index 339aea089be8..60c972a4147c 100644 --- a/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc +++ b/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc @@ -1,7 +1,7 @@ [[introduction-to-websocket]] = Introduction to WebSocket -The WebSocket protocol, https://tools.ietf.org/html/rfc6455[RFC 6455], provides a standardized +The WebSocket protocol, {rfc-site}/rfc6455[RFC 6455], provides a standardized way to establish a full-duplex, two-way communication channel between client and server over a single TCP connection. It is a different TCP protocol from HTTP but is designed to work over HTTP, using ports 80 and 443 and allowing re-use of existing firewall rules. @@ -46,7 +46,7 @@ A complete introduction of how WebSockets work is beyond the scope of this docum See RFC 6455, the WebSocket chapter of HTML5, or any of the many introductions and tutorials on the Web. -Note that, if a WebSocket server is running behind a web server (e.g. nginx), you +Note that, if a WebSocket server is running behind a web server (for example, nginx), you likely need to configure it to pass WebSocket upgrade requests on to the WebSocket server. Likewise, if the application runs in a cloud environment, check the instructions of the cloud provider related to WebSocket support. diff --git a/framework-docs/package.json b/framework-docs/package.json new file mode 100644 index 000000000000..1e0e629cc59c --- /dev/null +++ b/framework-docs/package.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "antora": "3.2.0-alpha.4", + "@antora/atlas-extension": "1.0.0-alpha.2", + "@antora/collector-extension": "1.0.0-alpha.3", + "@asciidoctor/tabs": "1.0.0-beta.6", + "@springio/antora-extensions": "1.14.2", + "fast-xml-parser": "4.5.2", + "@springio/asciidoctor-extensions": "1.0.0-alpha.10" + } +} diff --git a/framework-docs/src/docs/api/dokka-overview.md b/framework-docs/src/docs/api/dokka-overview.md new file mode 100644 index 000000000000..e68dd557ffc0 --- /dev/null +++ b/framework-docs/src/docs/api/dokka-overview.md @@ -0,0 +1,2 @@ +# All Modules +_See also the Java API documentation (Javadoc)._ \ No newline at end of file diff --git a/framework-docs/src/docs/api/overview.html b/framework-docs/src/docs/api/overview.html index e6086dc458d1..6e98d58b9c3d 100644 --- a/framework-docs/src/docs/api/overview.html +++ b/framework-docs/src/docs/api/overview.html @@ -1,7 +1,10 @@

-This is the public API documentation for the Spring Framework. +This is the public Java API documentation (Javadoc) for the Spring Framework.

+

+See also the Kotlin API documentation (KDoc). +

diff --git a/framework-docs/src/docs/asciidoc/anchor-rewrite.properties b/framework-docs/src/docs/asciidoc/anchor-rewrite.properties deleted file mode 100644 index e1e20afb8446..000000000000 --- a/framework-docs/src/docs/asciidoc/anchor-rewrite.properties +++ /dev/null @@ -1,9 +0,0 @@ -aot=core.aot -aot-basics=core.aot.basics -aot-refresh=core.aot.refresh -aot-bean-factory-initialization-contributions=core.aot.bean-factory-initialization-contributions -aot-bean-registration-contributions=core.aot.bean-registration-contributions -aot-hints=core.aot.hints -aot-hints-import-runtime-hints=core.aot.hints.import-runtime-hints -aot-hints-reflective=core.aot.hints.reflective -aot-hints-register-reflection-for-binding=core.aot.hints.register-reflection-for-binding \ No newline at end of file diff --git a/framework-docs/src/docs/asciidoc/images/DataAccessException.png b/framework-docs/src/docs/asciidoc/images/DataAccessException.png deleted file mode 100644 index 746f17399b99..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/DataAccessException.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/aop-proxy-call.png b/framework-docs/src/docs/asciidoc/images/aop-proxy-call.png deleted file mode 100644 index de6be86ed543..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/aop-proxy-call.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/aop-proxy-plain-pojo-call.png b/framework-docs/src/docs/asciidoc/images/aop-proxy-plain-pojo-call.png deleted file mode 100644 index 8ece077d3445..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/aop-proxy-plain-pojo-call.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/container-magic.png b/framework-docs/src/docs/asciidoc/images/container-magic.png deleted file mode 100644 index 2628e59b00e8..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/container-magic.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/message-flow-broker-relay.png b/framework-docs/src/docs/asciidoc/images/message-flow-broker-relay.png deleted file mode 100644 index 3cf93fa1439c..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/message-flow-broker-relay.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/message-flow-simple-broker.png b/framework-docs/src/docs/asciidoc/images/message-flow-simple-broker.png deleted file mode 100644 index 9afd54f57c23..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/message-flow-simple-broker.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.png b/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.png deleted file mode 100644 index 9c4a950caadb..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.svg b/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.svg deleted file mode 100644 index 07148744b549..000000000000 --- a/framework-docs/src/docs/asciidoc/images/mvc-context-hierarchy.svg +++ /dev/null @@ -1,612 +0,0 @@ - - - -image/svg+xmlPage-1DispatcherServlet -Servlet WebApplicationContext -(containing controllers, view resolvers,and other web-related beans) -Controllers -ViewResolver -HandlerMapping -Root WebApplicationContext -(containing middle-tier services, datasources, etc.) -Services -Repositories -Delegates if no bean found - \ No newline at end of file diff --git a/framework-docs/src/docs/asciidoc/images/oxm-exceptions.graffle b/framework-docs/src/docs/asciidoc/images/oxm-exceptions.graffle deleted file mode 100644 index 4b72bf45285b..000000000000 --- a/framework-docs/src/docs/asciidoc/images/oxm-exceptions.graffle +++ /dev/null @@ -1,1619 +0,0 @@ - - - - - ActiveLayerIndex - 0 - ApplicationVersion - - com.omnigroup.OmniGraffle - 137.11.0.108132 - - AutoAdjust - - BackgroundGraphic - - Bounds - {{0, 0}, {756, 553}} - Class - SolidGraphic - ID - 2 - Style - - shadow - - Draws - NO - - stroke - - Draws - NO - - - - CanvasOrigin - {0, 0} - ColumnAlign - 1 - ColumnSpacing - 36 - CreationDate - 2009-09-11 10:15:26 -0400 - Creator - Thomas Risberg - DisplayScale - 1 0/72 in = 1 0/72 in - GraphDocumentVersion - 6 - GraphicsList - - - Class - LineGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 42 - Points - - {334.726, 144} - {394.042, 102.288} - - Style - - stroke - - HeadArrow - FilledArrow - TailArrow - 0 - - - - - Class - LineGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 41 - Points - - {489.5, 143.713} - {430.452, 102.287} - - Style - - stroke - - HeadArrow - FilledArrow - TailArrow - 0 - - - - - Class - LineGraphic - FontInfo - - Font - Helvetica - Size - 18 - - Head - - ID - 4 - - ID - 40 - Points - - {230, 217} - {275.683, 175.337} - - Style - - stroke - - HeadArrow - FilledArrow - TailArrow - 0 - - - - - Class - LineGraphic - FontInfo - - Font - Helvetica - Size - 18 - - Head - - ID - 4 - - ID - 39 - Points - - {430.381, 216.81} - {329.369, 175.19} - - Style - - stroke - - HeadArrow - FilledArrow - TailArrow - 0 - - - Tail - - ID - 5 - - - - Bounds - {{56, 217}, {249, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 6 - Shape - Rectangle - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\fs36 \cf0 MarshallingFailureException} - - - - Bounds - {{325.5, 217}, {283.5, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 5 - Shape - Rectangle - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\fs36 \cf0 UnmarshallingFailureException} - - - - Bounds - {{184, 145}, {217, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 4 - Shape - Rectangle - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\fs36 \cf0 MarshallingException} - - - - Bounds - {{430, 145}, {239, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 3 - Shape - Rectangle - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\fs36 \cf0 ValidationFailureException} - - - - Bounds - {{294, 72}, {244, 30}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 18 - - ID - 1 - Shape - Rectangle - Style - - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\deftab720 -\pard\pardeftab720\qc - -\f0\i\fs36 \cf0 XmlMappingException} - - Wrap - NO - - - GridInfo - - GuidesLocked - NO - GuidesVisible - YES - HPages - 1 - ImageCounter - 1 - KeepToScale - - Layers - - - Lock - NO - Name - Layer 1 - Print - YES - View - YES - - - LayoutInfo - - Animate - NO - circoMinDist - 18 - circoSeparation - 0.0 - layoutEngine - dot - neatoSeparation - 0.0 - twopiSeparation - 0.0 - - LinksVisible - NO - MagnetsVisible - NO - MasterSheets - - ModificationDate - 2009-09-11 10:38:54 -0400 - Modifier - Thomas Risberg - NotesVisible - NO - Orientation - 2 - OriginVisible - NO - PageBreaks - YES - PrintInfo - - NSBottomMargin - - float - 41 - - NSLeftMargin - - float - 18 - - NSOrientation - - int - 1 - - NSPaperSize - - size - {792, 612} - - NSRightMargin - - float - 18 - - NSTopMargin - - float - 18 - - - PrintOnePage - - QuickLookPreview - - JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmls - dGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAGVlk1vG0cMhu/zK3h0Dx4Ph/N5rZsA - DRCgqdW0V0GVGhkryZGdoj+/L2e1q4W1cloLhhZrfr0PORx/pU/0lRw+OSaKUei4pt9p - T84m135oS3f3z0yrZ+L2eV7RrbPx9Nfz0ymAQYAN3f2yPq7WTy/flh0dt0jhU2ppoidf - hIIkWu3o7ucd00+HVoRPPFgEriRJTG/hRwupgwVnUYtTDBksxI1ZhION5Csqb3mCGfLk - c56pQeyD3P267pYv27/X94fucNzu1i/H7Uo1+BooRCYfAonrZTJ9AJPHntD9Q6vO0cM9 - BPdJbvVLsaIIDZAhv/kr5wfoBltvwNYRuDrAnngGThTADb4/LohLC3+L79tSbQwZDDMt - oO49W4eEi425+WPXfVw+PW33f737RxuwPex/oMUjvVsg2ZtNDeJIciEPyoO+STGjDLXj - AHLNbqpDZ2RORwwol6S2hr5Swi7aUpVMr8SflNDN53PdkzLeilWj9d7ly1DLbvsnenrY - v19uu2/H9Ws05jtouKDlioYz0KjkzbRPIxq1a2ianY7I0OJraHz1PZq5Jkc0WWqkbFqT - z2g+Lo/PX5Zd9/+7bMQjKkQkPYbt6bqc3lZFT20HSVenNmXrkcK3k/e63R6nMpZxcJsm - s9jQzW/73VnVlT59b4Sxwpqy8PYEw6yJamb/ZYC5YM2pIt1IrxWxWBdlZuomXZrTYy6O - 5GTMx5HCabNSOKDiZIvDNOxIpNjowZdzsTVxNd0waG1P6zpv51CELXtsH8nZuhJzc85W - cvV4x9anINQhYLUpKY6MJNwCfsHbS+8NAn/A7+Ps/I8enKOtjNg7I3LKx4VtFtQwycfI - x6Uw3k3ynb2brNOSNXN4PI6j9hLbNRUrSQ9gwVC5gpA6qT4H6zP2i0qT4kszxWNI1UgW - 6x2msYMdOOutU2zOIKYFzfleB6CzMXqosMTY9lpYnw3dqjaDyjkb9gU2VlAkk2yDr9lN - 5c8CD3oRYOWIzQzYuFYxGTHhlcu2ZqhtFEwQZZJxgWEVJ48rRG3Ra5GId9hBudUVgutp - hZBtKNI35sIblV3noJts9GAnmLaiHMZ8zM4G36gP+YxeA5FTbSTmvLWXw207NwgiwWaf - wCKgOimYP8DoORSvhDWCYN2Ggk3eODD+OVBbNAFDg3fQrBwxoCXjQNRoGpsk/TzMeb/N - YfRoHHB8W22nfE2zL+1AnPJRYyOpn4gLb1SrKj79C2PwIN4KZW5kc3RyZWFtCmVuZG9i - ago1IDAgb2JqCjkyNgplbmRvYmoKMiAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50 - IDMgMCBSIC9SZXNvdXJjZXMgNiAwIFIgL0NvbnRlbnRzIDQgMCBSIC9NZWRpYUJveCBb - MCAwIDc1NiA1NTNdCj4+CmVuZG9iago2IDAgb2JqCjw8IC9Qcm9jU2V0IFsgL1BERiAv - VGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSSBdIC9Db2xvclNwYWNlIDw8IC9DczIg - MTggMCBSCi9DczEgNyAwIFIgPj4gL0ZvbnQgPDwgL0YxLjAgMTkgMCBSIC9GMi4wIDIw - IDAgUiA+PiAvWE9iamVjdCA8PCAvSW0yIDEwIDAgUgovSW0zIDEyIDAgUiAvSW00IDE0 - IDAgUiAvSW01IDE2IDAgUiAvSW0xIDggMCBSID4+ID4+CmVuZG9iagoxMCAwIG9iago8 - PCAvTGVuZ3RoIDExIDAgUiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1hZ2UgL1dp - ZHRoIDUyMiAvSGVpZ2h0IDEwNCAvQ29sb3JTcGFjZQoyMSAwIFIgL1NNYXNrIDIyIDAg - UiAvQml0c1BlckNvbXBvbmVudCA4IC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVh - bQp4Ae3QMQEAAADCoPVPbQhfiEBhwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM - GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM - GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM - GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM - GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB - AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg - wIABAwYMGDBgwIABAwYMvAMDfE4AAQplbmRzdHJlYW0KZW5kb2JqCjExIDAgb2JqCjcz - NAplbmRvYmoKMTIgMCBvYmoKPDwgL0xlbmd0aCAxMyAwIFIgL1R5cGUgL1hPYmplY3Qg - L1N1YnR5cGUgL0ltYWdlIC9XaWR0aCA0NzggL0hlaWdodCAxMDQgL0NvbG9yU3BhY2UK - MjQgMCBSIC9TTWFzayAyNSAwIFIgL0JpdHNQZXJDb21wb25lbnQgOCAvRmlsdGVyIC9G - bGF0ZURlY29kZSA+PgpzdHJlYW0KeAHt0DEBAAAAwqD1T+1pCYhAYcCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYOADA0auAAEKZW5kc3RyZWFtCmVuZG9iagox - MyAwIG9iago2NzQKZW5kb2JqCjE0IDAgb2JqCjw8IC9MZW5ndGggMTUgMCBSIC9UeXBl - IC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggNjEyIC9IZWlnaHQgMTA0IC9D - b2xvclNwYWNlCjI3IDAgUiAvU01hc2sgMjggMCBSIC9CaXRzUGVyQ29tcG9uZW50IDgg - L0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7dCBAAAAAMOg+VNf4AiFUGHA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgIE/MOn+AAEKZW5kc3RyZWFtCmVuZG9iagoxNSAwIG9iago4NTYK - ZW5kb2JqCjE2IDAgb2JqCjw8IC9MZW5ndGggMTcgMCBSIC9UeXBlIC9YT2JqZWN0IC9T - dWJ0eXBlIC9JbWFnZSAvV2lkdGggNTQyIC9IZWlnaHQgMTA0IC9Db2xvclNwYWNlCjMw - IDAgUiAvU01hc2sgMzEgMCBSIC9CaXRzUGVyQ29tcG9uZW50IDggL0ZpbHRlciAvRmxh - dGVEZWNvZGUgPj4Kc3RyZWFtCngB7dAxAQAAAMKg9U9tCy+IQGHAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAED - BgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDA - gAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwY - MGDAgAEDBgy8BwaUrgABCmVuZHN0cmVhbQplbmRvYmoKMTcgMCBvYmoKNzYxCmVuZG9i - ago4IDAgb2JqCjw8IC9MZW5ndGggOSAwIFIgL1R5cGUgL1hPYmplY3QgL1N1YnR5cGUg - L0ltYWdlIC9XaWR0aCA1MzIgL0hlaWdodCAxMDQgL0NvbG9yU3BhY2UKMzMgMCBSIC9T - TWFzayAzNCAwIFIgL0JpdHNQZXJDb21wb25lbnQgOCAvRmlsdGVyIC9GbGF0ZURlY29k - ZSA+PgpzdHJlYW0KeAHt0DEBAAAAwqD1T20KP4hAYcCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG - DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA - AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw - YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMDAa2CIfgABCmVuZHN0 - cmVhbQplbmRvYmoKOSAwIG9iago3NDcKZW5kb2JqCjIyIDAgb2JqCjw8IC9MZW5ndGgg - MjMgMCBSIC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggNTIyIC9I - ZWlnaHQgMTA0IC9Db2xvclNwYWNlCi9EZXZpY2VHcmF5IC9CaXRzUGVyQ29tcG9uZW50 - IDggL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7Z3tT1PZFsZBCqXvLZS2 - 9GVaTgvtaSmdY4sFCtM2bXhHFISpM0LQqhkYkNHYSAZ1MIwSiSI4EF6iyBDRgEPAECVG - zfxrd53CvXOFcrD3fto96/lATDYmez/rl7XXaTlrZWWh0AF0AB1AB9ABdAAdQAfQAXQA - Hfh/HMhGZaADaREB5z/xj3JQGeHAPxE9AQH+CiD2KICzCwS5qIxzQCCA0LJQHAdDkoM9 - CPKEwvw9iVDEO7AfSqEwD+AGHI5hYZ+D3Nw8gEAkFkskEqlUKkNlgAMQSAinWCzKz2dp - 4GaBBSEH7gTAACCQyuRyhVKpQmWIA0qlQi6XAQ9igGGPhSOuiCQIkA9YDmRyhUpVUKhW - FxVpNFoU8Q5oNEVFanVhgUqlkMtYFiAvwBWRGgU2I7AJgeVACRRotLpivd5gNJpQxDtg - NBr0+mKdVgM0KJMsQFpgUUjxEJEEAQoEiRQ4AAyAAZPZYimhrKgMcIAqsVjMJuABYAAW - pBK2XEiNQjZbIwhFkBBUhRqdHiigrKVldgdNO50uFNEOOJ007bCXlVopoEGv0xSqIC2I - hGzdeDgpQEoAEPIlMoVKrdWbLJStjHaWuz0ehmFOogh3AILo8bjLnXSZjbKY9Fq1SiGD - rJArSHE/QEqAYlGcBMFgpkodLreH8Vae8lfXgAIogh1gI1jtP1XpZTxul6OUMhuSKIih - bEyRFLIhJeSLpXKVWmcwW+2uCsbnrw7UBUPhSCQSRRHtAIQwHArWBar9PqbCZbeaDTq1 - Si4V50NSOHg97KUECYCgNVhstJvxVQWC4Wh9Y1NLa9tpFOEOtLW2NDXWR8PBQJWPcdM2 - C5sV5JJUSYElAe4GJYBgttEer782FGlobmvv6OzqjqGId6C7q7Ojva25IRKq9Xs9tI29 - IJQySAqHrge4HPLyJfICjd5spSt8NcFoU+vZc7Efe3r7LsXjl1FEOxCPX+rr7fkxdu5s - a1M0WOOroK1mvaaATQqHrofsE/AECSlBZ6Lsbm9NqL7lTNf5nr741f6fB4euDaOIduDa - 0ODP/VfjfT3nu8601IdqvG47ZdJBUoAnyYOFAns5QJWg0VtKXYw/WN/aEbtw8Ur/4PCN - m4lbIyjCHbiVuHljeLD/ysULsY7W+qCfcZVa9Bq2UoDr4cuPGZMkKAq1JspR4auNAgi9 - 8f6h64mR0Tt3x+6hCHdg7O6d0ZHE9aH+eC+gEK31VTgok7ZQkZKEPJFUqS4221xMVajp - TKz38sBwYuTO2Pj9iYeTKMIdeDhxf3zszkhieOByb+xMU6iKcdnMxWqlVJR3KCcI8kQy - 9nIoc/sCkbauC/GBXxKjY79PTD5+Mv0URbgD008eT078Pjaa+GUgfqGrLRLwucvY60Em - gpLxwO0gEIrlBVoj5fD4v2s4e/7iT8OJ0XsPJqdmZucWFhZRRDuwsDA3OzM1+eDeaGL4 - p4vnzzZ85/c4KKO2QC4WpiBBIocywepkqsPN53quDAIIE4+mZ+eXni2/WEER7cCL5WdL - 87PTjyYAhcErPeeaw9WM0wqFglySggR4dFAXf1Na7oXLIdbXf33ktwePZuYWn6+svlx7 - hSLagbWXqyvPF+dmHj34beR6f18Mrgdveek3xWp4eDiUE+AhUqFmy4TKuvr2H+KDidvj - k9NzS8t/rr1e33iDItqBjfXXa38uL81NT47fTgzGf2ivr6tkCwU1+/BwsE4AEpRAgt3j - DzZ29FwdHhmbmJpdXF59tfFmc2sbRbQDW5tvNl6tLi/OTk2MjQxf7eloDPo9diBBmZIE - qbJIXwIFY6ips7f/xq/jkzPzzwGEze23OyjCHXi7vQkoPJ+fmRz/9UZ/b2dTCErGEn2R - UpoqJ0hVRQaK/rY63NLVN3Dz9v3HfyytrK1vbu+8e7+LItqB9+92tjfX11aW/nh8//bN - gb6ulnD1tzRlKFIdQYLGSNFMTaS1+9Jg4u7E1Nyz1dd/be282/2AItyB3Xc7W3+9Xn02 - NzVxNzF4qbs1UsPQlFFzNAnwEAkkfB8fujX28Mn88sv1zbcAwsdPKKId+Phh993bzfWX - y/NPHo7dGop/z5LgtB5LQlssfm3k3uTMwou1ja2d9wDCZxTRDnz6+OH9ztbG2ouFmcl7 - I9fi8Bh5FAnwpXS+VKUxJnNCChL+RhHswGduEr74+7XsnFz42gE+YnSdDERPxy4PQ054 - urjy6s32zu6HT58JdgG3Dg58/vRhd2f7zauVxaeQE4Yvx05HAydd8CEjfPGQm4Mk8AcS - JIE/seY+KZLA7Q9/VpEE/sSa+6RIArc//FlFEvgTa+6TIgnc/vBnFUngT6y5T4okcPvD - n1UkgT+x5j4pksDtD39WkQT+xJr7pEgCtz/8WUUS+BNr7pMiCdz+8GcVSeBPrLlPiiRw - +8OfVSSBP7HmPimSwO0Pf1aRBP7EmvukSAK3P/xZRRL4E2vukyIJ3P7wZxVJ4E+suU+K - JHD7w59VJIE/seY+KZLA7Q9/VtMhAd+QzWAu0nlDNusYEoh+Zxw3z/2u9IHOnP/11jx2 - 0iC6bUaKzf8PnTSwuw7hbXSO2H663XWw4xbRfbWO3ny6HbewCx/hvfaO3n56XfiwMyfR - 3Te5Np9mZ07s1kt0R16uzafVrVeAHbyJbtLNufm0OngLhNjVn+jO/VybT6+rP076IHqY - B+fm05v0gdN/CJ/ww7X9dKb/5OBEMMKnfnFtP52JYOy8SJwSSPgwwCO3n9aUQJwcSvh0 - UK7tpzE5FKcJEz0u+JjNpzNNGCeMEz1C/JjNpzdhHAoFMYwY1xrMNtrj9deGIg3Nbe0d - nV3dMRTxDnR3dXa0tzU3REK1fq+HtpkNWhgwLmbHSn/RwDsrKxsGS+exM8YBBYuNdjO+ - qkAwHK1vbGppbTuNItyBttaWpsb6aDgYqPIxbtpmARDY+eJ5h0kAFASQFKSAgs5gttpd - FYzPXx2oC4bCkUgkiiLaAQhhOBSsC1T7fUyFy241G3QAghRSguBgSvh3UhDLFGxWMFOl - Dpfbw3grT/mra0ABFMEOsBGs9p+q9DIet8tRSrFXg0oBd0OqlAA5AZKCMF+SREFvslC2 - MtpZ7vZ4GIY5iSLcAQiix+Mud9JlNspi0idBkOQLISUczglspQAoiCQyuapQo9ObzBbK - Wlpmd9C00+lCEe2A00nTDntZqZWymE16naZQJZdJRADCoXqR/etWSApQNEJWkMqVBWqN - Vm8wAg2WEsqKygAHqBILUGA06LUadYFSLoWMwN4NKVLCPgpwQYghLSgLCgEGXbEeeDCa - UMQ7YAQG9MU6wKAQOJBJxHA1HAVCVnYyK8CzZJIFhUoFNKiLijQaLYp4BzSaoiI1UKBS - KZIcQLGYBOHAhwn7rz4kURDkQloAFiRSmVyuUCpVqAxxQKlUyOUyqQTyAZsQoEY4kZ0a - BLgfICuwdSNbLuSLxICDRCqVylAZ4AAEEsIpFosAA8gHLAdHg8CWjXssAAxAA+CQlAhF - vAP7oRSyFOQKjuUg+QjBsnAiJydHwOKAyjAHAIIcNh1w5oP9aoFNDEka2N8Hwf9EZYAD - e9FM/oQA/yfYX/MP+H1UxjnwNZHH30EH0AF0AB1AB9ABdAAdQAfQAXTgaAf+BYU9EtcK - ZW5kc3RyZWFtCmVuZG9iagoyMyAwIG9iagoyNzE5CmVuZG9iagoyOCAwIG9iago8PCAv - TGVuZ3RoIDI5IDAgUiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1hZ2UgL1dpZHRo - IDYxMiAvSGVpZ2h0IDEwNCAvQ29sb3JTcGFjZQovRGV2aWNlR3JheSAvQml0c1BlckNv - bXBvbmVudCA4IC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae2d7U9T2RaH - BQql7y2U03LaTutpS3taS+fYaoXitKRNESm+oDh1FIJWzVSLHY2NzaAOxlEi8Q1HghhF - xohGHSKGqDGjmX/trl3MnTt290jvyf101+8DMfvI/vDkyVq7B9hrwwYMEkACSAAJIAEk - gASQABJAAkgACSCB/wWBOgwSWD+B2hSEfev/TgMGCYgR+FuVejBnPaqt+QV7ymSNGCSw - XgIyGThDdPuqZmXD1vRqksub16LAIIFqBD47Ipc3gY0g2tcs+2xYY2MT6KVQKlUqlVqt - 1mCQQHUCYAh4olQqmpuJZ1+xjCjWAB0SBAO91BqtVqfXGzBIQJyAXq/TajVgmhI0W7Os - WsMsKwY1jBim0eoMhpZWo7GtjWFMGCRQjQDDtLUZja0tBoNOqyGWQS2DhllFMlLFSBEj - hunBL8ZkbmdZi9VqwyCBagSsVgvLtptNDHimL1sGpYxIRvuAWVYMDmIqNRgGgoFdNrvD - sZFzYpBAdQLcRofDbgPTQDOwTK0ix7IqktWRs5hcAUXM0MqYWfCLc7o7PF6e9/n8GCRA - I+Dz8bzX0+F2cuAZa2ZaDVDKFHJy8qcUMihjoFizSqMzGE2szcG5OnjfpkAwKAjCZgwS - oBMAO4LBwCYf3+HiHDbWZDToNFDJGmW0bgllDI77yrJiFjvn9voDQSG0ZWukqxsSxSCB - SgJEja7I1i0hIRjwe92c3VKWTAkHf1ohq4My1qxUaw1Gs8Xu9Pg7hXCkK7o9Fu9NJBJJ - DBKgEQA3euOx7dGuSFjo9HucdovZaNCqlc1QyCqa5VoZU4FiJovDxQeE8LZorDeZ2tE/ - kB7chUECdAKD6YH+Halkbyy6LSwEeJeDVDKtilrIiGPQKfWgmN3FB0ORnniib+fg7qF9 - wwcyGCRQjcCB4X1Duwd39iXiPZFQkHeRdqnXQCGrbJbQKpuaVdoWhrU7+c5wdyzZn967 - P3NoZHTsaDZ7DIMEaASy2aNjoyOHMvv3pvuTse5wJ++0s0wLKWSVzbKuHt5bQBkz2zhP - INQdTw3sGT44MpY9kTuVHz9dwCABGoHT4/lTuRPZsZGDw3sGUvHuUMDD2cxQyOD9RcWB - jLRKOI0xrMPtFyKxVHooc/jI8Vy+cPZc8XwJgwToBM4Xz50t5HPHjxzODKVTsYjgdztY - hpzIoFl+8aq/7Jiu1WTjvJ3hniQoNprNjZ8pliYuXpq8jEECdAKTly5OlIpnxnPZUZAs - 2RPu9HI2U6uO7liTQq03tttdfmFbvH9PZvTYyUKxdHHyytWp69MYJEAncH3q6pXJi6Vi - 4eSx0cye/vg2we+ytxv1akVTZR2TNSk0pFV2BMLRxODw4ezJn4oTk79OTd+8fecuBgnQ - Cdy5fXN66tfJieJPJ7OHhwcT0XCggzRLjQIO/V/2SplcqW0xWTlvMPJd396DR34sFCcu - X5u+NXNv9v79eQwSoBG4f3/23syt6WuXJ4qFH48c3Nv3XSTo5aymFq1STnNMpYXjmNMn - dPXu3D9yPA+KTd24c2/uwcOFx4sYJEAj8Hjh4YO5e3duTIFk+eMj+3f2dgk+JxzItCqa - Y/Cx0tj+jXtTCFplZix3pvTLtRszs/OPFp88XXqGQQI0AktPnyw+mp+duXHtl9KZ3FgG - mmVok/ubdiN8sKysY/DqQmckx7Et21O7f8jmixeuTN+ZfbDw+9LzFy9fYZAAjcDLF8+X - fl94MHtn+sqFYj77w+7U9i3kQGYkHywrzmPgmB4c8wQjsR1DIycKpcmpW/fmF548e/lq - +fUKBgnQCLxefvXy2ZOF+Xu3piZLhRMjQztikaAHHNPTHVPr29iNcOSP9+8bzZ39+cr0 - zNwjUGx55c0qBgnQCbxZWQbJHs3NTF/5+WxudF9/HA79G9k2vZpax9SGNgvHf9vVOzA8 - dvLchas3f3uwuPRieWX17bv3GCRAI/Du7erK8oulxQe/3bx64dzJseGB3q5vec7SZqjm - GGPleKE7kT5wNF+8NHVr9uGT53+8Xn37/gMGCdAJvH+7+vqP508ezt6aulTMHz2QTnQL - PGdlRByDVxfg2PfZ8fOT12/PLTx9sfwGFPvzIwYJ0Aj8+eH92zfLL54uzN2+Pnl+PPs9 - cczn/Lpjg5ns6dLl6Zn7j5devl59B4p9wiABGoGPf354t/r65dLj+zPTl0uns/Dyoqpj - 8Ks9zWoDYy3XMYpjf2GQQCWBT+KO/fO3resaGuHHlfCa3785mtyVOVaAOnZ3fvHZq5XV - 9x8+fqrcHVeQABD49PHD+9WVV88W5+9CHSscy+xKRjf74UU//MCysQEdQ0mkE0DHpDPE - HcQJoGPifPCpdALomHSGuIM4AXRMnA8+lU4AHZPOEHcQJ4COifPBp9IJoGPSGeIO4gTQ - MXE++FQ6AXRMOkPcQZwAOibOB59KJ4COSWeIO4gTQMfE+eBT6QTQMekMcQdxAuiYOB98 - Kp0AOiadIe4gTgAdE+eDT6UTQMekM8QdxAmgY+J88Kl0AuiYdIa4gzgBdEycDz6VTgAd - k84QdxAngI6J88Gn0gmgY9IZ4g7iBNAxcT74VDoBdEw6Q9xBnAA6Js4Hn0onUJNjeKeK - dOD/fzvUdKfKhq84RrsXCNeQgPi9PV/OgPiPu6HwjjvadW64RiHw39xxh3d10q+kxNUq - BGq+qxPvHKbdq4tr1QnUfOcw3p1Ovx8cV6sTqPHudJwBQZtygGtiBGqdAYGzbGjTWnBN - jEBts2xkOJOLNnQK10QJ1DaTSybH2YK06Xm4JkagxtmCOCOVNgQU10QJ1DgjFWc906cZ - 46oYgZpmPTfgzHr6VHZcFSNQ08z6BjIkFYY9c97OcE8yPZQZzebGzxRLExcvTV7GIAE6 - gclLFydKxTPjuexoZiid7Al3ejkY9UxGpDZUzEgljmkNDOtw+4VILAWSHT5yPJcvnD1X - PF/CIAE6gfPFc2cL+dzxI4dBsVQsIvjdDpYxwKjnSsdgYJJcodEbzTbOEwh1x1MDe4YP - joxlT+RO5cdPFzBIgEbg9Hj+VO5Edmzk4PCegVS8OxTwcDazUa9RyBvr/znKZsOGunpZ - ExSyFoa1O/nOcHcs2Z/euz9zaGR07Gg2ewyDBGgEstmjY6MjhzL796b7k7HucCfvtLNM - C5SxJhnFMWiWSihkJovdxQdDkZ54om/n4O6hfcMHMhgkUI3AgeF9Q7sHd/Yl4j2RUJB3 - 2S0mKGNK0ior61hDIylkBpDM4eIDQnhbNNabTO3oH0gP7sIgATqBwfRA/45UsjcW3RYW - ArzLAYqR01gTxTHSLKGQqUEys8Xu9Pg7hXCkK7o9Fu9NJBJJDBKgEQA3euOx7dGuSFjo - 9HucdosZFFNDGatsleRARgqZUqMjlczOub3+QFAIbdka6eqGRDFIoJIAUaMrsnVLSAgG - /F43RxqlQQedklrGwDEoZPJmVVky1ubgXB28b1MgGBQEYTMGCdAJgB3BYGCTj+9wcQ4b - W1ZM1SyHMlZxHIPf7odCBpIpVBqtoZUxsza7g3O6Ozxenvf5/BgkQCPg8/G819PhdnIO - u401M60GrUYF7y1klSd+8gckUMigW0IlU2v1LUbGxFqs4JljI+fEIIHqBLiNDvDLamFN - jLFFr1VDFSOdklbGPksG7VIJpUzf0gqamdtZMM1qwyCBagSsYBfbbgbBWsEwjUoJjbKq - YhvqypUMDv5ly3QGA3hmbGtjGBMGCVQjwDBtbUbwy2DQlQ2D435ZsS9fjn3+W8uyZLJG - KGVgmUqt0Wp1er0BgwTECej1Oq1Wo1ZBDSNFDM5i9XVVFINuCZWMnPzJsaxZoQTRVGq1 - WoNBAtUJgCHgiVKpAMGghhHDRBQjB/81y0Az8AxEK0eBQQLVCHx2RE78apR93bDyx0ti - WX1DQ4OMiIZBAusjAHo1kBImXsM+n8pIMSt7Rr4BAt+KQQLVCaxpUv4K5vzbonX9A74B - gwTWS2BdTuF/QgJIAAkgASSABJAAEkACSAAJIAEkUDuBfwFWtww3CmVuZHN0cmVhbQpl - bmRvYmoKMjkgMCBvYmoKMzAwNwplbmRvYmoKMzEgMCBvYmoKPDwgL0xlbmd0aCAzMiAw - IFIgL1R5cGUgL1hPYmplY3QgL1N1YnR5cGUgL0ltYWdlIC9XaWR0aCA1NDIgL0hlaWdo - dCAxMDQgL0NvbG9yU3BhY2UKL0RldmljZUdyYXkgL0JpdHNQZXJDb21wb25lbnQgOCAv - RmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAHtnetPU+kWxrkUer9B2S29TMtu - ueyW0tlSLFCclrQBykXkOnUUghTNwICMxkYyqINhlEgUwYFwiSJDRAMOAUOUGDXzr521 - CzlzhLKZnk/nvHs9H4zJWz+sZ/1ca+1e3pWWhkIH0AF0AB1AB9ABdAAdQAfQAXTg/9GB - dJRAHEiJTvAk429looh14O8sZ0DS/wEkB2SAHyJRFkoQDohEkG4OlNMASbBxAEa2WCw5 - kBRFpAOH6RWLs+E/ASByCh+HbGRlZQMYUplMLpcrFAolilAHILmQYplMKpFwhPDzwcGR - Cf0E0AAwFEqVSq3RaFEEO6DRqFUqJTAiA0AO+DihvSTggLrBsaFUqbXanFydLi+PovQo - Ih2gqLw8nS43R6tVq5QcH1A/oL0kx4OrHFzh4NjQABmU3pBvNJrMZguKSAfMZpPRmG/Q - U0CIJsEHlA8OjyQPLwk4YOCQK4ANQAO4sFhttgLajiLUAbrAZrNagBEABPhQyLnxIzke - 6dzMIZZC4dDmUgYjkEHbC4uKSxjG6XShiHPA6WSYkuKiQjsNhBgNVK4WyodUzM2mx4sH - lA6AQyJXqrU6vdFiox1FjLPU7fGwLHsGRaADkFiPx13qZIoctM1i1Ou0aiVUjyxRkt4C - pQMGUlkCDpOVLixxuT1secVZX1U1yI8izAEuq1W+sxXlrMftKimkraYEHjIYTZMUj3Qo - HRKZQqXVGUxWe7GrjPX6qvznAsHaUCgURhHnAKS1Nhg456/yedkyV7HdajLotCqFTALF - 42hrOSgdcoBDb7I5GDfrrfQHasN1DZGm5pbzKAIdaGluijTUhWsD/kov62YcNq56qOTJ - igdHB/QVDcBhdTCecl9NMFTf2NLa3tHVHUUR6UB3V0d7a0tjfShY4yv3MA6uuWiUUDyO - tRZoLNkSuSqHMlrtTJm3OhCONLd1Ri/19Pb1x2IDKOIciMX6+3p7LkU725oj4UC1t4yx - W41UDlc8jrWW9Ax4moXSYbDQxe7y6mBd04Wuiz19sWuDPw2PXB9FEefA9ZHhnwavxfp6 - LnZdaKoLVpe7i2mLAYoHPNUeHTy4xgJTB2W0FbpYX6CuuT16+crVweHRm7fit8dQBDpw - O37r5ujw4NUrl6PtzXUBH+sqtBkpbvKA1vL126UJOtS5egtdUuatCQMcvbHBkRvxsfG7 - 9ybuowh0YOLe3fGx+I2RwVgv4BGu8ZaV0BZ9rjopHdlShUaXb3W42Mpg5EK0d2BoND52 - d2LywdSjaRSBDjyaejA5cXcsPjo00Bu9EAlWsi6HNV+nUUizj9UOUbZUyTWWIrfXH2rp - uhwb+jk+PvHb1PSTp7PPUAQ6MPv0yfTUbxPj8Z+HYpe7WkJ+r7uIay1KKYylRzqLSCxT - 5ejNdInH911928UrP47Gx+8/nJ6Zm19YWlpGEefA0tLC/NzM9MP74/HRH69cbKv/zucp - oc36HJVMnIQOuQrGDruTrapt7Oy5OgxwTD2enV9ceb76cg1FnAMvV5+vLM7PPp4CPIav - 9nQ21laxTjsMHip5EjrgkUWX/01haTk0lmjf4I2xXx8+nltYfrG2/mrjNYo4BzZera+9 - WF6Ye/zw17Ebg31RaC3lpYXf5OvgoeVY7YAHWrWOGzsqztW1/hAbjt+ZnJ5dWFn9Y+PN - 5tZbFHEObG2+2fhjdWVhdnryTnw49kNr3bkKbvDQcQ8tR+cOoEMDdBR7fIGG9p5ro2MT - UzPzy6vrr7febu/soohzYGf77dbr9dXl+ZmpibHRaz3tDQGfpxjo0CSlQ6HJMxbAUBqM - dPQO3vxlcnpu8QXAsb37bg9FoAPvdrcBjxeLc9OTv9wc7O2IBGEsLTDmaRTJaodCm2ei - mW+rapu6+oZu3Xnw5PeVtY3N7d299x/2UcQ58OH93u725sbayu9PHty5NdTX1VRb9S1D - m/K0J9BBmWmGrQ41d/cPx+9NzSw8X3/z587e+/2PKAId2H+/t/Pnm/XnCzNT9+LD/d3N - oWqWoc3UyXTAAy3Q8X1s5PbEo6eLq682t98BHJ8+o4hz4NPH/ffvtjdfrS4+fTRxeyT2 - PUeH034qHS3R2PWx+9NzSy83tnb2PgAcX1DEOfD508cPeztbGy+X5qbvj12PwSPtSXTA - B/gShZYyJ2pHEjr+QhHmwBd+Or767mB6ZhZ8zAJvlbrO+MPnowOjUDueLa+9fru7t//x - 8xfCnMFwwIEvnz/u7+2+fb22/Axqx+hA9HzYf8YFb5bCBy1ZmUiHsCFBOoSdf/7okQ5+ - f4R9inQIO//80SMd/P4I+xTpEHb++aNHOvj9EfYp0iHs/PNHj3Tw+yPsU6RD2Pnnjx7p - 4PdH2KdIh7Dzzx890sHvj7BPkQ5h558/eqSD3x9hnyIdws4/f/RIB78/wj5FOoSdf/7o - kQ5+f4R9inQIO//80SMd/P4I+xTpEHb++aNHOvj9EfYp0iHs/PNHj3Tw+yPsU6RD2Pnn - jx7p4PdH2Kep0IG/shYYK6n8yjrtFDqIu58AA+L/Df6R22z/44YGvN2FuKtckgT0X9zu - gjdDEXgF1AkhpXozFN4qR9zdcScHlOqtcngjJYH3Tp4cUmo3UuJttsTdWMsXUIq32eJN - 2MTdds0XUEo3YYvwFn3iLsrnDSilW/RFYtzAQdyWDb6AUtvAgdt7iFvQwxtQatt7cPMX - gdu9+EJKZfNXJm4NJHAzIF9IqWwN5PbR4sZRAheLnhhSShtHcVsxgRuJ+UJKYVsxbjon - bpX5KQGlsuk8PUOUDe945FBGq50p81YHwpHmts7opZ7evv5YbABFnAOxWH9fb8+laGdb - cyQcqPaWMXarkcqBnYGwjvara9LT0tK5VecypUanN1kdjKfcVxMM1Te2tLZ3dHVHUUQ6 - 0N3V0d7a0lgfCtb4yj2Mw2rS6zRKmSQrMxkdXPHQAh42B+NmvZX+QG24riHS1NxyHkWg - Ay3NTZGGunBtwF/pZd2MwwZwaLnScZwOKB4iKB4KwMNgstqLXWWs11flPxcI1oZCoTCK - OAcgrbXBwDl/lc/LlrmK7VaTAeBQQOk41lgOWks29BY1Vz2sdGGJy+1hyyvO+qqqQX4U - YQ5wWa3yna0oZz1uV0khzbUVrRr6SrLSAbUDiodYIk/gYbTYaEcR4yx1ezwsy55BEegA - JNbjcZc6mSIHbbMYE3DIJWIoHUfHDviSKcylgIdUrlRpcymD0WK10fbCouIShnE6XSji - HHA6GaakuKjQTtusFqOBytWqlHIpwHFsJuW+gQzFIzMrG6qHQqXJ0VF6o8kMhNgKaDuK - UAfoAhuQYTYZ9ZQuR6NSQOXg+kqS0nGIBzQXGZQPTU4uAGLINwIjZguKSAfMwIUx3wBo - 5AIbSrkM2spJcKSlJ6pHVrYkwYdaqwVCdHl5FKVHEekAReXl6YAMrVadYAMG0gQcR94K - O/xpSwIPURaUD+BDrlCqVGqNRosi2AGNRq1SKRVyqBtc4YCZIyM9ORzQW6B6cLMpN35I - pDJARK5QKJQoQh2A5EKKZTIpoAF1g2PjZDi40fSADwAECAFEEpKiiHTgML1ijows0als - JB5dOD4yMjMzRRwiKAE4AGBkcmWDt24cTh9cAUkQwr0eBP8SRagDBxlO/AlJ/zcA/+Qv - 8HqUIBz4JzTga9ABdAAdQAfQAXQAHUAH0AF0AB3433PgX6y7qcQKZW5kc3RyZWFtCmVu - ZG9iagozMiAwIG9iagoyNzYyCmVuZG9iagoyNSAwIG9iago8PCAvTGVuZ3RoIDI2IDAg - UiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1hZ2UgL1dpZHRoIDQ3OCAvSGVpZ2h0 - IDEwNCAvQ29sb3JTcGFjZQovRGV2aWNlR3JheSAvQml0c1BlckNvbXBvbmVudCA4IC9G - aWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae2d/U8T2RfGeSn0fToD7bRM222Z - Uui0lO4IWAFdIBAUAV9Q3LorBK2ahQW7GhubRV0Mq8RGEVwIL1FkiWjAJWCIErOa/de+ - Z4rZXaEdvt2f7iTn+cFoLiaH58Nz723pnJOTg0IH0AF0AB1AB9ABdAAdQAfQgf/mQC5K - IQ5kxRe+p7x/lI8i1oF/KOUBtP8D8g5Z+H5UqgKUIhxQqQCXBHo/wCm2O2AL1WrNjrQo - Ih34jEetLoQfQkC8D9/PbAsKCgGsVqfT6/UGg8GIItQBgAOIdDqtRiMRlucrwc2H/RjQ - AliDkaJMNM2gCHaApk0UZQTGOgC8wzfD9pyCC7mV2BopE8MUFZvNFgvLWlFEOsCyFovZ - XFzEMCbKKPGF/ML2nB6vlFwpuBJbGsiyVlsJx9kdDieKSAccDjvHldisLBCmU3whvhLe - NJfnFFw4cPUGYAtogavT5XaX8h4UoQ7wpW63ywmMATDwNeil4zc93lzpzFVrIbhMMWvj - gCzv8ZZX+ATB7w+giHPA7xcEX0W518MDYc7GFjMQX61aulvtDS9EF+Bq9EYTY7ZyTjdf - Vi74K4OhkCiKB1AEOgBgQqFgpV8oL+PdTs5qZkxGSG+BKs3eDNGFC5UuBdfu4r2+QDAk - VtceDNfVgxpQhDkgUakLH6ytFkPBgM/Lu+wpvDq4WqUJby5EV6MzUIzZZnd5KgJVYk24 - ruFIY1NzS0tLK4o4BwBLc1PjkYa6cI1YFajwuOw2M0MZdBoI7+6teSe6eoBrtbvLhKBY - c6ihsbm17Vh7R2fXCRSBDnR1drQfa2ttbmw4VCMGhTK3lF5Kny68El3Yl2mA6yoTQtXh - w00tR493new+03MugiLSgXM9Z7pPdh0/2tJ0OFwdEsqkzZk2Qnj3bM2wMRdq9FQRy7k8 - QlVNfWNre+fps5Hve/v6L0Wjl1HEORCNXurv6/0+cvZ0Z3trY31NleBxcWyRFN49W3Nu - HrwagujanHxFsLq+qa3jVM/53v7o1YEfh4avxVDEOXBteOjHgavR/t7zPac62prqq4MV - vNMG4YVXRbsPXmljhlOX5dzegBhubOvsjly4eGVgKHbjZvxWAkWgA7fiN2/EhgauXLwQ - 6e5sawyLAa+bY6WTF7bmL9+uStE1FVudvK+q5nArwO2LDgxfjydG7twdvYci0IHRu3dG - EvHrwwPRPsDberimysc7rcWmtHQLtQbaXOIqC4iHmtpPRfouD8biiTujY/fHHyZRBDrw - cPz+2OidRDw2eLkvcqq96ZAYKHOVmGmDtnBPdlWFWqO0MZcHaxpaunouRAd/io+M/jqe - fPxk8imKQAcmnzxOjv86OhL/aTB6oaerpaEmWC5tzUYtXKt27cwqtY4qsjp4Xyj8zdHT - 5y/+EIuP3HuQnJianpmbm0cR58Dc3Mz01ETywb2ReOyHi+dPH/0mHPLxDmsRpVOnoaun - 4Nj1+MW65uNne68MAdzxR5PTswvPFl8soYhz4MXis4XZ6clH44B36Erv2ePNdaLfAwcv - pU9DF67M5pKvvJXVsDFH+geuJ3558GhqZv750vLLlVco4hxYebm89Hx+ZurRg18S1wf6 - I7A1V1d6vyoxw6V5T3bhBZHJLB27tUfaTn4XHYrfHktOziws/r7yenXtDYo4B9ZWX6/8 - vrgwM5kcux0fin53su1IrXTwmqVL8+5zF+jSQLciFG481t17NZYYHZ+Ynl9cfrX2Zn1j - E0WcAxvrb9ZeLS/OT0+MjyZiV3u7jzWGQxVAl05L10BbuFK4VDW1n+kbuPHzWHJq9jnA - Xd98u4Ui0IG3m+uA9/nsVHLs5xsDfWfam+BaVcpZaEO67BoYi50Xvq5r7ujpH7x5+/7j - 3xaWVlbXN7fevd9GEefA+3dbm+urK0sLvz2+f/vmYH9PR3Pd1wJvtzAZ6LIOXhDrWzrP - XRqK3x2fmHm2/PqPja132x9QBDqw/W5r44/Xy89mJsbvxocunetsqRcF3sFmpgsviIDu - t9HhW6MPn8wuvlxdfwtw//yIIs6BPz9sv3u7vvpycfbJw9Fbw9FvJbp+z750uyLRa4l7 - yam5FytrG1vvAe4nFHEOfPzzw/utjbWVF3NTyXuJa1F4SZSJLvwCUGNgWEcqu2no/oUi - zIFP8nS/+OxNbn4BvM0Mb1UFDjS0nohcjkF2n84vvXqzubX94eMnwr4zLAcc+PTxw/bW - 5ptXS/NPIbuxy5ETrQ0HAvBmFbzRXJCPdJX9Q4J0lc1PvnqkK++PsleRrrL5yVePdOX9 - UfYq0lU2P/nqka68P8peRbrK5idfPdKV90fZq0hX2fzkq0e68v4oexXpKpuffPVIV94f - Za8iXWXzk68e6cr7o+xVpKtsfvLVI115f5S9inSVzU++eqQr74+yV5GusvnJV4905f1R - 9irSVTY/+eqRrrw/yl5FusrmJ1890pX3R9mrSFfZ/OSrR7ry/ih7NRu6+JSYwlhn85RY - zj50iXu+EQuSfwZwVzeyfz3hiU9nE/codpqC/sPT2dhZgcAWChlKyrazAnZFIa73SeaC - su2Kgh2NCOxblLmk7DoaYTcy4jqOyRWUZTcy7CRIXLdAuYKy6iSowi6gxDX6lC0oqy6g - KjV28CWuS69cQdl18MXu28Q12JYtKLvu29g5n8Du+HIlZdM5Px+nXhA42UKupGymXkjz - iHBiDYGDaTKWlNXEGpw2ReBEKbmSspg2hZPiiBsFt09B2UyKwymPxI1x3Keg7KY84oRW - IsewyhSVzYRWaTA6TlcmcIpyppKymK6cg5PRiZt9Ll9QNpPRga4UXp3RJM1Gd/FeXyAY - EqtrD4br6kENKMIckKjUhQ/WVouhYMDn5aXB2YwJJmcX7h2dnQN081QFao0+hZdzuvmy - csFfGQyFRFE8gCLQAQATCgUr/UJ5Ge92cim4eo26QJW3e7gyfMgKwgt4tXojxRSzNs7p - cvMeb3mFTxD8/gCKOAf8fkHwVZR7Pbzb5eRsbDFDGfUwN1u1Z+q99Ak6CC/szZBeA0UX - mVkrZ3cAYXcp70ER6gBf6gayDjtnZc1FNGWA5Er7cprofsYLm7MO4ksXFQNgWwkHjB1O - FJEOOIArV2IDtMXA1qjXwbacCW5Obiq9cLVK8TUxDBA2Wywsa0UR6QDLWixmIMswphRb - uFCl4H4x8eLvDzan8KoKIL7AV28wUpSJphkUwQ7QtImijAY95FYKLpy5ebnp4cLeDOmV - 7lbS8avR6gCx3mAwGFGEOgBwAJFOpwW0kFuJbWa40tVqhy8ABsKAOCUtikgHPuNRS2QL - VPuyTV2dJb55+fn5KgkxSgEOANh8Kbayuf3X+ZsiLH09CP4nilAHdgil/szNeN7+zfWL - v8DXoxThwBfY8B/oADqADqAD6AA6gA6gA+gAOpCFA/8DclEtHwplbmRzdHJlYW0KZW5k - b2JqCjI2IDAgb2JqCjI1NTgKZW5kb2JqCjM0IDAgb2JqCjw8IC9MZW5ndGggMzUgMCBS - IC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggNTMyIC9IZWlnaHQg - MTA0IC9Db2xvclNwYWNlCi9EZXZpY2VHcmF5IC9CaXRzUGVyQ29tcG9uZW50IDggL0Zp - bHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7Z3rT1PpFsZBCqX3Fnqjl2nZbaG7 - pXS2LZZSmLZpwx1REKbOCEGrZmBARmMjGdTBMEokiuBAuESRIaIBh4AhSoya86+dtQvn - zBHKxp7k5CTvXs8HY7Lxw3rWL8+7drHvyslBoQPoADqADqAD6AA6gA6gA+gAOvD/ciAX - RbQDWXEFTpz4W3kowhz4u7cnoNVfgcYeD+CCQJCPItgBgQCazOJxHBZpIvZwKBAKC/ck - QhHkwH5ThcICAB7AOIaKfSLy8wsAB5FYLJFIpFKpDEWUA9BSaKxYLCosZLngpoJFIg9O - DAACcJDK5HKFUqlCEeeAUqmQy2VAhhiw2KPiiAMkjQRkBEuETK5QqYqK1WqNRqvVoQhy - QKvVaNTq4iKVSiGXsVRAVsABkhkKNiXYkGCJUAIPWp2+xGAwmkxmFEEOmExGg6FEr9MC - F8o0FRAVLBQZXkDSSMAgIZECEQAE0GC2WK2llA1FlANUqdVqMQMZgAVQIZWwY0VmKHLZ - WUIogpBQFWv1BuCBsjnKyp007XK5UYQ44HLRtLO8zGGjgAuDXlusgqgQCdlJ83BQQEwA - EoUSmUKl1hnMVspeRrsqPF4vwzAnUcQ4AO30ej0VLrrMTlnNBp1apZBBUuQLMpweEBMw - XorTSBgtlMPp9ngZX9WpQLAGFEIR4QDby2DgVJWP8XrcTgdlMaahEMOgmSEociEmCsVS - uUqtN1ps5e5Kxh8IhurCkWgsFoujCHEAmhmNhOtCwYCfqXSX2yxGvVoll4oLISgOHh57 - MSEBJHRGq532MP7qUDgar29samltO40ixoG21pamxvp4NByq9jMe2m5lk0IuyRQULBNw - cigBCYud9voCtZFYQ3Nbe0dnV3cCRZAD3V2dHe1tzQ2xSG3A56Xt7PGhlEFQHDo84Ogo - KJTIi7QGi42u9NeE402tZ88lfuzp7buUTF5GEeJAMnmpr7fnx8S5s61N8XCNv5K2WQza - IjYoDh0euSfgPRRiQm+myj2+mkh9y5mu8z19yav9Pw8OXRtGEeLAtaHBn/uvJvt6zned - aamP1Pg85ZRZD0EB76MHBwr26IBpQmuwOtxMIFzf2pG4cPFK/+DwjZupWyMoYhy4lbp5 - Y3iw/8rFC4mO1vpwgHE7rAYtO1HA4fHlR5lpJhTFOjPlrPTXxgGJ3mT/0PXUyOidu2P3 - UMQ4MHb3zuhI6vpQf7IXoIjX+iudlFlXrMjIRIFIqlSXWOxupjrSdCbRe3lgODVyZ2z8 - /sTDSRQxDjycuD8+dmckNTxwuTdxpilSzbjtlhK1UioqOJQTggKRjD06yjz+UKyt60Jy - 4JfU6NjvE5OPn0w/RRHjwPSTx5MTv4+Npn4ZSF7oaouF/J4y9vCQiWDIPHB2CIRieZHO - RDm9ge8azp6/+NNwavTeg8mpmdm5hYVFFCEOLCzMzc5MTT64N5oa/uni+bMN3wW8Tsqk - K5KLhRmYkMhhnLC5mGC0+VzPlUFAYuLR9Oz80rPlFysoQhx4sfxsaX52+tEEQDF4pedc - czTIuGwwUMglGZiA1w51yTeOCh8cHYm+/usjvz14NDO3+Hxl9eXaKxQhDqy9XF15vjg3 - 8+jBbyPX+/sScHj4KhzflKjhxeNQTsCrqELNjhNVdfXtPyQHU7fHJ6fnlpb/XHu9vvEG - RYgDG+uv1/5cXpqbnhy/nRpM/tBeX1fFDhRq9sXj4DwBTCiBiXJvINzY0XN1eGRsYmp2 - cXn11cabza1tFCEObG2+2Xi1urw4OzUxNjJ8taejMRzwlgMTyoxMSJUaQymMmJGmzt7+ - G7+OT87MPwckNrff7qCIceDt9iZA8Xx+ZnL81xv9vZ1NERgySw0apTRTTkhVGiNFfxuM - tnT1Ddy8ff/xH0sra+ub2zvv3u+iCHHg/bud7c31tZWlPx7fv31zoK+rJRr8lqaMGtUR - TGhNFM3UxFq7Lw2m7k5MzT1bff3X1s673Q8oYhzYfbez9dfr1WdzUxN3U4OXultjNQxN - mbRHMwGvosDE98mhW2MPn8wvv1zffAtIfPyEIsSBjx92373dXH+5PP/k4ditoeT3LBMu - 27FMtCWS10buTc4svFjb2Np5D0h8RhHiwKePH97vbG2svViYmbw3ci0JL6NHMQG/Ki+U - qrSmdE5kYOIfKCIc+MzNxBf/+y43Lx9+3QEfY7pPhuKnE5eHISeeLq68erO9s/vh02ci - /MAiwIHPnz7s7my/ebWy+BRyYvhy4nQ8dNINH2TCLzzy85AJPkKCTPCx69w1IxPc/vDx - KTLBx65z14xMcPvDx6fIBB+7zl0zMsHtDx+fIhN87Dp3zcgEtz98fIpM8LHr3DUjE9z+ - 8PEpMsHHrnPXjExw+8PHp8gEH7vOXTMywe0PH58iE3zsOnfNyAS3P3x8ikzwsevcNSMT - 3P7w8Skywceuc9eMTHD7w8enyAQfu85dMzLB7Q8fnyITfOw6d83IBLc/fHyKTPCx69w1 - IxPc/vDxaTZM4HeIeUFINt8hzjmGCUK+aY9lcH+v/MCdqf9x1wDeSULIBSQZyvgv7iTB - u4uIuaToiEKyvbsI7zgj5Cazo8vI9o4zvAuRmBsPjy4ku7sQ8c5UQu5F5SojyztT8W5l - Qu5P5iojq7uVBXgHOyHXrHOWkdUd7AIh7mogZB8DVxnZ7WrAnS6ErG3hLCO7nS64+4mY - /U5chWSz+ykPd8QRsweOq5BsdsSx+0VxlyQxKyOPLCSrXZK4c5aYvbJchWSxcxZ3UxOy - fPqYMrLZTY077AlZUn9MGdntsIeBQgxL7HVGi532+gK1kVhDc1t7R2dXdwJFkAPdXZ0d - 7W3NDbFIbcDnpe0Wow5W2IvZdeVfXMGek5MLC8sL2C32AIXVTnsYf3UoHI3XNza1tLad - RhHjQFtrS1NjfTwaDlX7GQ9ttwIS7Ab7gsNMABQCCAopQKE3Wmzl7krGHwiG6sKRaCwW - i6MIcQCaGY2E60LBgJ+pdJfbLEY9ICGFmBAcjIl/BYVYpmCTwkI5nG6Pl/FVnQoEa0Ah - FBEOsL0MBk5V+Rivx+10UOzBoVLAyZEpJiAnICiEhZI0FAazlbKX0a4Kj9fLMMxJFDEO - QDu9Xk+Fiy6zU1azIY2EpFAIMXE4J9iJAqAQSWRyVbFWbzBbrJTNUVbupGmXy40ixAGX - i6ad5WUOG2W1mA16bbFKLpOIAIlDEyb7/3UhKGDMhKSQypVFaq3OYDQBF9ZSyoYiygGq - 1Ao8mIwGnVZdpJRLISXYkyNDTOxDAceHGKJCWVQMWOhLDECGyYwiyAET0GAo0QMQxUCE - TCKGg+MoJHJy00kBb6RpKhQqFXCh1mi0Wh2KIAe0Wo1GDTyoVIo0ETBeppE48OHE/lc9 - 0lAI8iEqgAqJVCaXK5RKFYo4B5RKhVwuk0ogI9iQgFniRG5mJOD0gKRgJ012rCgUiQEM - iVQqlaGIcgBaCo0Vi0UABGQES8TRSLCD5h4VgAVwAWCkJUIR5MB+U4UsD/mCY4lIv36w - VJzIy8sTsGCgiHUAcMhjI4IzI/anCjYs0lywPw+Cf4kiyoG9vqb/hFb/u+1f8xf4eRTB - DnwNA/gz6AA6gA6gA+gAOoAOoAPoADqADvxvHPgnR1HeRgplbmRzdHJlYW0KZW5kb2Jq - CjM1IDAgb2JqCjI3MDkKZW5kb2JqCjM2IDAgb2JqCjw8IC9MZW5ndGggMzcgMCBSIC9O - IDMgL0FsdGVybmF0ZSAvRGV2aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0 - cmVhbQp4AYWUTUgUYRjH/7ONBLEG0ZcIxdDBJFQmC1IC0/UrU7Zl1UwJYp19d50cZ6eZ - 3S1FIoTomHWMLlZEh4hO4aFDpzpEBJl1iaCjRRAFXiK2/zuTu2NUvjAzv3me//t8vcMA - VY9SjmNFNGDKzrvJ3ph2enRM2/waVahGFFwpw3M6EokBn6mVz/Vr9S0UaVlqlLHW+zZ8 - q3aZEFA0KndkAz4seTzg45Iv5J08NWckGxOpNNkhN7hDyU7yLfLWbIjHQ5wWngFUtVOT - MxyXcSI7yC1FIytjPiDrdtq0ye+lPe0ZU9Sw38g3OQvauPL9QNseYNOLim3MAx7cA3bX - VWz1NcDOEWDxUMX2PenPR9n1ysscavbDKdEYa/pQKn2vAzbfAH5eL5V+3C6Vft5hDtbx - 1DIKbtHXsjDlJRDUG+xm/OQa/YuDnnxVC7DAOY5sAfqvADc/AvsfAtsfA4lqYKgVkcts - N7jy4iLnAnTmnGnXzE7ktWZdP6J18GiF1mcbTQ1ayrI03+VprvCEWxTpJkxZBc7ZX9t4 - jwp7eJBP9he5JLzu36zMpVNdnCWa2NantOjqJjeQ72fMnj5yPa/3GbdnOGDlgJnvGwo4 - csq24jwXqYnU2OPxk2TGV1QnH5PzkDznFQdlTN9+LnUiQa6lPTmZ65eaXdzbPjMxxDOS - rFgzE53x3/zGLSRl3n3U3HUs/5tnbZFnGIUFARM27zY0JNGLGBrhwEUOGXpMKkxapV/Q - asLD5F+VFhLlXRYVvVjhnhV/z3kUuFvGP4VYHHMN5Qia/k7/oi/rC/pd/fN8baG+4plz - z5rGq2tfGVdmltXIuEGNMr6sKYhvsNoOei1kaZ3iFfTklfWN4eoy9nxt2aPJHOJqfDXU - pQhlasQ448muZfdFssU34edby/av6VH7fPZJTSXXsrp4Zin6fDZcDWv/s6tg0rKr8OSN - kC48a6HuVQ+qfWqL2gpNPaa2q21qF9+OqgPlHcOclYkLrNtl9Sn2YGOa3spJV2aL4N/C - L4b/pV5hC9c0NPkPTbi5jGkJ3xHcNnCHlP/DX7MDDd4KZW5kc3RyZWFtCmVuZG9iagoz - NyAwIG9iago3OTIKZW5kb2JqCjcgMCBvYmoKWyAvSUNDQmFzZWQgMzYgMCBSIF0KZW5k - b2JqCjM4IDAgb2JqCjw8IC9MZW5ndGggMzkgMCBSIC9OIDMgL0FsdGVybmF0ZSAvRGV2 - aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae1XiTvU2xs/YwnZ - 953JPoylsSY72UNIlqwzjG0GM3bZomyhRIgsIZHsW0KSkq0ikiSUJFfRvT9Kda97ht99 - +j2/5/YfdL7Pe87nvO857znfed/n+3kHAFacvo2NJQ0AgEAMJdmaGCAdnZyRdDOAHtAC - Btize2LJwZQ1cMlP2tYzgKCYnqJN7I8a3DcdqHu3Sc2SPdrIp7lelPyTTf+oWXDeZCwA - CDRUeJDg4QBQcUHMhd/DMhTstYe1KJhkb2sI19hAYcLvYpqTFOy1i/f5UnBEaHAoAPRQ - ABc2mETBxRCf9QrY1adS9OFYPNQzSACwjwPr64kDgFUD6tE4AhFixDmIbXEEHAUvQIwh - BIbBe+42ytsyeROP28HRCIoQMAVhwBtEARtgC6wBEhiCIBAIhQSxJZwZwVEG9mpAFSIT - gAEKUJBAHyhBpAQf1E/8q+36dwTy0LMfCIWnIIEZIAIs3EfxGbH7KIBIeNaeXQHaggAB - oGBM//3OPLs+/+2OSgBQ4g/tsBH5ATCg/HY8P3TOvQBkwpgIon/o5L8DoOQFQMM7bBgp - fG8v4p+BGtABRsAO+OBtUUAFvrEVcIX3TwC5oAb0gefgdwQzQhZhhvBDZCAaEJOIb1QS - VNZUMVQ1VDPU+6m1qUnUNdSvaYRonGjyaaZp+Wldaa/Sru5T3he3b4ROgC6Arpeek96f - /h6DMEM0w8z+Q/uLGRGMeMYnTJpM1cw8zGeZt1mILG9Z3Vhn2BzYptgd2Gc43DiWOYM4 - v3GlcfNz1/Lo8kzxEvjo+Sr5DfmXBFIE0YKTQnHCKOFpkRRRTdGPyKoDbmL8YtPilyQc - JYUk30jdkA6XMURxoJZkO+Sy0D7yegrCCt8VXyndPViNOa8crYJXdVAzVdfSwByS1ZQ8 - LKZ1QFtcR1pXQU9NX9/A2tDdKOTIGeNikzbTcbN1C1ZLzNETVvHWNTZTtjR2qva+x0sc - ph3Znaycs1yeuHK5ubhXemx46WCzca991PHZvu/9jQOqCHTEgKCJkMOk6lDusDPhXyKJ - UcsxHqfm4lzj5xOxp1eTyWd2UrLSRNObz5lmLmafusCfcyv35CVEfk2hXREobijxLOMu - H604W2VQjbjeX3umzrKeu2Ghqb4lvs22Q+YW6Jrt7uwt6Ivp9xgwfYB5iBxmHUWMbT3+ - OL7y9O3U0vTyzOrsp7lvC/SveZdklrVX7FcJa2kfr28M//5xk/uzzrb/t8I/h3d2fuXC - r1z4lQu/vgv//134wRtbU/+tG+R+6EA0JI44KL4/4S5/aBPa5ddgyLcUzsMDX8iLFC7E - QoZBgv/lSjScY3b59SBk0D2kvsuc+pCfA6GVwqp7Hsi7M29AhhxLAuGwx8GVYK9O2KMz - QI2A5QWsCAhUMzTGtM10ovQZDF8YcUzjLNqstew8HEmc69wneTx58XyB/CSBCMFYodPC - qSKZoheRhQdKxCrFayQaJC9JxUp7y1iilGUF5ajk3qPH5TsVShVTlIgHj2O0laVU2FS+ - qr5RG1Pv0Cg7lK5JPuyqZaKtpCOgS6P7QW9N/zeD94YrRu+OvDVeMnltumi2YP7KYs7y - 5dEXVjPWqzZfbRnthOzRx7UdrE64OQY5JTifdyk/2eza7/bUfcFjzXMbS4vj8BbxkcOr - +xr52fi7BvgHhhOSiNlBVcG3Q8ZJy+TvYRzhMhE6kXZRftHxMXmnbsT2xT2Ln0h4nDhy - ejDpXnLfme6znSmtqY1pN9NrMqrOlWeWZJ3O9jlvfkExhzPn88WXuX15lZfS8gkFxwpV - Lwtc/qvoZXHXlfySsFK7MqVylvK1q8MV1ZVnqrDX9KtFqr9fn6vpqb1yI67O7aZuvWj9 - TsNCY19TeXNii1erQZtYO1X7YkdfZ+mtuC7X21rdgt1fe2Z6O+/k94XfdehXvUd3b2ag - 7n7cA5tB8cHNh4NDhcOBIzqjbKNvxtoepTw2ekL15M74qYlDE1tPWyaDp+SnVp9VT3s/ - F3s+P1P8wnoWMdv6Ej8nMDf2KmEeM/92oWDRYvGv141vsEs8S71vfZaZl9veua7QrNS9 - t3//fbXiN4vf/li7/MHgwyqMvwKVC3UaTQftEh0HvR4DYX8R4xDTFosEqy1bAvtNjlRO - LJcOrC3+w/OIt4Yvmd9dQFOQW3BDaES4SiRBVFC0HXkMuXYgRUxMrEfcUfwPiSxJlOSA - lLvUV+k8GQOZDdQVWQvZL3LX0HbyCPl6BRdFBsV2JdxB9oN3MCRlSeXnKhmqOqq/q1Wp - O2owavQeCtGU0Hx+OENLR5tGe0gnW/e4nqDeon61QaAhxvCLUe+RZGMzE1aTKdNGs3Rz - bwtdSwHLzaOPrWqtk23cjx2y5bJdtxuyrzye6RBxwsPRzEnZWchln8v6yRmYM3XueR5x - nr5ex7AoHAI3493ok4r38NXwY/V7538nID8wiGBMFCFuBfUHZ4U4kSRJn8g9oWlh9uHI - 8LWIzsjkKOtoweh3MUOn6mKz4oLj7RJUE3kTv55+mdSbXH4m6ax3immqbOp8Wl66eQbI - aDsXkInMfJaVka2fvXW+7oJnDm/O6MWMXPM8+rzBS2fzjQqoC/oLEy7rwozqLo65onnl - S0lLKaFMumyxvPiqQwVbxTDMKt2q7Wst1cTrMtdf16TXKtfO3kiqk6ubvHmqXqJ+rCGs - UbjxfhOxmae5tyWglbd1oC24Xbh9qCO8U6zz8a2YLpmuyduJ3fLdL3oyerV71++U9dne - pb7b1u9zj+/e0EDMffT9Vw+yB/Vh/LWoIqlbaTb2oel86SsZFhiFmByZc1mesDGxm3Ik - c+K5zLjRPCw8n3if8rXzFwjECLoJ6QmLi9CKrIgOIyMPSB14KpYorij+UiJdUkNyWeqi - tL70ukwxyhz1Rfa6nCOaHn1L3k+BX2FIMUoJpfTiYDpGE7OqfFnFXJVFdVwtD8ZdSGP+ - UIWmz2HZwx+0mrRDddR1vun26hXohxhYGEoY/mU0faTJON0EZ6ptxmu2YT5sUWEZe5Rs - 5WftYeNw7KitoZ2mvdJxaQfhE5yO+52A02fnDy6zJx+6trtVul/wiPckeDljTXFq3uI+ - 7D47+DXfF34T/iMBA4HdhDZifdC14NKQAtJ5clro6bDocHJEYGRgFCGaGBN0Kig2OC4k - npRASiSfDk0KTYbF6dmIFNdUwzRUOmv6ZsbsubuZ1VmZ2eTzThd0cyQvMl78lDuV13Wp - JD+pAF9ocVmpiKtou3juSn/JtdL0sqByu6smFZqV8lXIa1zVdNVfr3+oWaydvDFYd/tm - fX15Q25jYVNpc2VLTWt9W0t7Z0dPZ/+twa6x2xPd0z0vexfvfL/L2698z2rA737yg9LB - 2w+fDX0aYRqVHjN85PY46knReM/E/CRiSvyZ8bTv8/SZuhePZjfmOF6pzDssRL7OWapb - frCysPr1A9e64iezP7CbsZ/zt5u+jfz5dmcHALKPMmaXERDM2wDQLUFSgMTABP8fbh7Y - 2dnZghnivrPzJzdACIX/Ddo0yhkKZW5kc3RyZWFtCmVuZG9iagozOSAwIG9iagoyNjM0 - CmVuZG9iagoyNyAwIG9iagpbIC9JQ0NCYXNlZCAzOCAwIFIgXQplbmRvYmoKNDAgMCBv - YmoKPDwgL0xlbmd0aCA0MSAwIFIgL04gMyAvQWx0ZXJuYXRlIC9EZXZpY2VSR0IgL0Zp - bHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngB7VeJO9TbGz9jCdn3nck+jKWxJjvZ - Q0iWrDOMbQYzdtmibKFEiCwhkexbQpKSrSKSJJQkV9G9P0p1r3uG3336Pb/n9h90vs97 - zue87znvOd953+f7eQcAVpy+jY0lDQCAQAwl2ZoYIB2dnJF0M4Ae0AIG2LN7YsnBlDVw - yU/a1jOAoJieok3sjxrcNx2oe7dJzZI92sinuV6U/JNN/6hZcN5kLAAINFR4kODhAFBx - QcyF38MyFOy1h7UomGRvawjX2EBhwu9impMU7LWL9/lScERocCgA9FAAFzaYRMHFEJ/1 - CtjVp1L04Vg81DNIALCPA+vriQOAVQPq0TgCEWLEOYhtcQQcBS9AjCEEhsF77jbK2zJ5 - E4/bwdEIihAwBWHAG0QBG2ALrAESGIIgEAiFBLElnBnBUQb2akAVIhOAAQpQkEAfKEGk - BB/UT/yr7fp3BPLQsx8IhacggRkgAizcR/EZsfsogEh41p5dAdqCAAGgYEz//c48uz7/ - 7Y5KAFDiD+2wEfkBMKD8djw/dM69AGTCmAiif+jkvwOg5AVAwztsGCl8by/in4Ea0AFG - wA744G1RQAW+sRVwhfdPALmgBvSB5+B3BDNCFmGG8ENkIBoQk4hvVBJU1lQxVDVUM9T7 - qbWpSdQ11K9phGicaPJppmn5aV1pr9Ku7lPeF7dvhE6ALoCul56T3p/+HoMwQzTDzP5D - +4sZEYx4xidMmkzVzDzMZ5m3WYgsb1ndWGfYHNim2B3YZzjcOJY5gzi/caVx83PX8ujy - TPES+Oj5KvkN+ZcEUgTRgpNCccIo4WmRFFFN0Y/IqgNuYvxi0+KXJBwlhSTfSN2QDpcx - RHGglmQ75LLQPvJ6CsIK3xVfKd09WI05rxytgld1UDNV19LAHJLVlDwspnVAW1xHWldB - T01f38Da0N0o5MgZ42KTNtNxs3ULVkvM0RNW8dY1NlO2NHaq9r7HSxymHdmdrJyzXJ64 - crm5uFd6bHjpYLNxr33U8dm+7/2NA6oIdMSAoImQw6TqUO6wM+FfIolRyzEep+biXOPn - E7GnV5PJZ3ZSstJE05vPmWYuZp+6wJ9zK/fkJUR+TaFdEShuKPEs4y4frThbZVCNuN5f - e6bOsp67YaGpviW+zbZD5hbomu3u7C3oi+n3GDB9gHmIHGYdRYxtPf44vvL07dTS9PLM - 6uynuW8L9K95l2SWtVfsVwlraR+vbwz//nGT+7POtv+3wj+Hd3Z+5cKvXPiVC7++C/// - XfjBG1tT/60b5H7oQDQkjjgovj/hLn9oE9rl12DItxTOwwNfyIsULsRChkGC/+VKNJxj - dvn1IGTQPaS+y5z6kJ8DoZXCqnseyLszb0CGHEsC4bDHwZVgr07YozNAjYDlBawICFQz - NMa0zXSi9BkMXxhxTOMs2qy17DwcSZzr3Cd5PHnxfIH8JIEIwVih08KpIpmiF5GFB0rE - KsVrJBokL0nFSnvLWKKUZQXlqOTeo8flOxVKFVOUiAePY7SVpVTYVL6qvlEbU+/QKDuU - rkk+7Kploq2kI6BLo/tBb03/N4P3hitG7468NV4yeW26aLZg/spizvLl0RdWM9arNl9t - Ge2E7NHHtR2sTrg5BjklOJ93KT/Z7Nrv9tR9wWPNcxtLi+PwFvGRw6v7GvnZ+LsG+AeG - E5KI2UFVwbdDxknL5O9hHOEyETqRdlF+0fExeaduxPbFPYufSHicOHJ6MOlect+Z7rOd - Ka2pjWk302syqs6VZ5Zknc72OW9+QTGHM+fzxZe5fXmVl9LyCQXHClUvC1z+q+hlcdeV - /JKwUrsypXKW8rWrwxXVlWeqsNf0q0Wqv1+fq+mpvXIjrs7tpm69aP1Ow0JjX1N5c2KL - V6tBm1g7VftiR19n6a24LtfbWt2C3V97Zno77+T3hd916Fe9R3dvZqDuftwDm0Hxwc2H - g0OFw4EjOqNso2/G2h6lPDZ6QvXkzvipiUMTW09bJoOn5KdWn1VPez8Xez4/U/zCehYx - 2/oSPycwN/YqYR4z/3ahYNFi8a/XjW+wSzxLvW99lpmX2965rtCs1L23f/99teI3i9/+ - WLv8weDDKoy/ApULdRpNB+0SHQe9HgNhfxHjENMWiwSrLVsC+02OVE4slw6sLf7D84i3 - hi+Z311AU5BbcENoRLhKJEFUULQdeQy5diBFTEysR9xR/A+JLEmU5ICUu9RX6TwZA5kN - 1BVZC9kvctfQdvII+XoFF0UGxXYl3EH2g3cwJGVJ5ecqGao6qr+rVak7ajBq9B4K0ZTQ - fH44Q0tHm0Z7SCdb97ieoN6ifrVBoCHG8ItR75FkYzMTVpMp00azdHNvC11LAcvNo4+t - aq2TbdyPHbLlsl23G7KvPJ7pEHHCw9HMSdlZyGWfy/rJGZgzde55HnGevl7HsCgcAjfj - 3eiTivfw1fBj9XvnfycgPzCIYEwUIW4F9QdnhTiRJEmfyD2haWH24cjwtYjOyOQo62jB - 6HcxQ6fqYrPiguPtElQTeRO/nn6Z1JtcfibprHeKaaps6nxaXrp5BshoOxeQicx8lpWR - rZ+9db7ugmcOb87oxYxc8zz6vMFLZ/ONCqgL+gsTLuvCjOoujrmieeVLSUspoUy6bLG8 - +KpDBVvFMMwq3artay3VxOsy11/XpNcq187eSKqTq5u8eapeon6sIaxRuPF+E7GZp7m3 - JaCVt3WgLbhduH2oI7xTrPPxrZguma7J24nd8t0vejJ6tXvX75T12d6lvtvW73OP797Q - QMx99P1XD7IH9WH8tagiqVtpNvah6XzpKxkWGIWYHJlzWZ6wMbGbciRz4rnMuNE8LDyf - eJ/ytfMXCMQIugnpCYuL0IqsiA4jIw9IHXgqliiuKP5SIl1SQ3JZ6qK0vvS6TDHKHPVF - 9rqcI5oefUveT4FfYUgxSgml9OJgOkYTs6p8WcVclUV1XC0Pxl1IY/5QhabPYdnDH7Sa - tEN11HW+6fbqFeiHGFgYShj+ZTR9pMk43QRnqm3Ga7ZhPmxRYRl7lGzlZ+1h43DsqK2h - naa90nFpB+ETnI77nYDTZ+cPLrMnH7q2u1W6X/CI9yR4OWNNcWre4j7sPjv4Nd8XfhP+ - IwEDgd2ENmJ90LXg0pAC0nlyWujpsOhwckRgZGAUIZoYE3QqKDY4LiSelEBKJJ8OTQpN - hsXp2YgU11TDNFQ6a/pmxuy5u5nVWZnZ5PNOF3RzJC8yXvyUO5XXdakkP6kAX2hxWamI - q2i7eO5Kf8m10vSyoHK7qyYVmpXyVchrXNV01V+vf6hZrJ28MVh3+2Z9fXlDbmNhU2lz - ZUtNa31bS3tnR09n/63BrrHbE93TPS97F+98v8vbr3zPasDvfvKD0sHbD58NfRphGpUe - M3zk9jjqSdF4z8T8JGJK/JnxtO/z9Jm6F49mN+Y4XqnMOyxEvs5Zqlt+sLKw+vUD17ri - J7M/sJuxn/O3m76N/Pl2ZwcAso8yZpcREMzbANAtQVKAxMAE/x9uHtjZ2dmCGeK+s/Mn - N0AIhf8N2jTKGQplbmRzdHJlYW0KZW5kb2JqCjQxIDAgb2JqCjI2MzQKZW5kb2JqCjI0 - IDAgb2JqClsgL0lDQ0Jhc2VkIDQwIDAgUiBdCmVuZG9iago0MiAwIG9iago8PCAvTGVu - Z3RoIDQzIDAgUiAvTiAzIC9BbHRlcm5hdGUgL0RldmljZVJHQiAvRmlsdGVyIC9GbGF0 - ZURlY29kZSA+PgpzdHJlYW0KeAHtV4k71NsbP2MJ2fedyT6MpbEmO9lDSJasM4xtBjN2 - 2aJsoUSILCGR7FtCkpKtIpIklCRX0b0/SnWve4bfffo9v+f2H3S+z3vO57zvOe8533nf - 5/t5BwBWnL6NjSUNAIBADCXZmhggHZ2ckXQzgB7QAgbYs3tiycGUNXDJT9rWM4CgmJ6i - TeyPGtw3Hah7t0nNkj3ayKe5XpT8k03/qFlw3mQsAAg0VHiQ4OEAUHFBzIXfwzIU7LWH - tSiYZG9rCNfYQGHC72KakxTstYv3+VJwRGhwKAD0UAAXNphEwcUQn/UK2NWnUvThWDzU - M0gAsI8D6+uJA4BVA+rROAIRYsQ5iG1xBBwFL0CMIQSGwXvuNsrbMnkTj9vB0QiKEDAF - YcAbRAEbYAusARIYgiAQCIUEsSWcGcFRBvZqQBUiE4ABClCQQB8oQaQEH9RP/Kvt+ncE - 8tCzHwiFpyCBGSACLNxH8Rmx+yiASHjWnl0B2oIAAaBgTP/9zjy7Pv/tjkoAUOIP7bAR - +QEwoPx2PD90zr0AZMKYCKJ/6OS/A6DkBUDDO2wYKXxvL+KfgRrQAUbADvjgbVFABb6x - FXCF908AuaAG9IHn4HcEM0IWYYbwQ2QgGhCTiG9UElTWVDFUNVQz1PuptalJ1DXUr2mE - aJxo8mmmaflpXWmv0q7uU94Xt2+EToAugK6XnpPen/4egzBDNMPM/kP7ixkRjHjGJ0ya - TNXMPMxnmbdZiCxvWd1YZ9gc2KbYHdhnONw4ljmDOL9xpXHzc9fy6PJM8RL46Pkq+Q35 - lwRSBNGCk0JxwijhaZEUUU3Rj8iqA25i/GLT4pckHCWFJN9I3ZAOlzFEcaCWZDvkstA+ - 8noKwgrfFV8p3T1YjTmvHK2CV3VQM1XX0sAcktWUPCymdUBbXEdaV0FPTV/fwNrQ3Sjk - yBnjYpM203GzdQtWS8zRE1bx1jU2U7Y0dqr2vsdLHKYd2Z2snLNcnrhyubm4V3pseOlg - s3GvfdTx2b7v/Y0Dqgh0xICgiZDDpOpQ7rAz4V8iiVHLMR6n5uJc4+cTsadXk8lndlKy - 0kTTm8+ZZi5mn7rAn3Mr9+QlRH5NoV0RKG4o8SzjLh+tOFtlUI243l97ps6ynrthoam+ - Jb7NtkPmFuia7e7sLeiL6fcYMH2AeYgcZh1FjG09/ji+8vTt1NL08szq7Ke5bwv0r3mX - ZJa1V+xXCWtpH69vDP/+cZP7s862/7fCP4d3dn7lwq9c+JULv74L//9d+MEbW1P/rRvk - fuhANCSOOCi+P+Euf2gT2uXXYMi3FM7DA1/IixQuxEKGQYL/5Uo0nGN2+fUgZNA9pL7L - nPqQnwOhlcKqex7IuzNvQIYcSwLhsMfBlWCvTtijM0CNgOUFrAgIVDM0xrTNdKL0GQxf - GHFM4yzarLXsPBxJnOvcJ3k8efF8gfwkgQjBWKHTwqkimaIXkYUHSsQqxWskGiQvScVK - e8tYopRlBeWo5N6jx+U7FUoVU5SIB49jtJWlVNhUvqq+URtT79AoO5SuST7sqmWiraQj - oEuj+0FvTf83g/eGK0bvjrw1XjJ5bbpotmD+ymLO8uXRF1Yz1qs2X20Z7YTs0ce1HaxO - uDkGOSU4n3cpP9ns2u/21H3BY81zG0uL4/AW8ZHDq/sa+dn4uwb4B4YTkojZQVXBt0PG - Scvk72Ec4TIROpF2UX7R8TF5p27E9sU9i59IeJw4cnow6V5y35nus50pramNaTfTazKq - zpVnlmSdzvY5b35BMYcz5/PFl7l9eZWX0vIJBccKVS8LXP6r6GVx15X8krBSuzKlcpby - tavDFdWVZ6qw1/SrRaq/X5+r6am9ciOuzu2mbr1o/U7DQmNfU3lzYotXq0GbWDtV+2JH - X2fprbgu19ta3YLdX3tmejvv5PeF33XoV71Hd29moO5+3AObQfHBzYeDQ4XDgSM6o2yj - b8baHqU8NnpC9eTO+KmJQxNbT1smg6fkp1afVU97Pxd7Pj9T/MJ6FjHb+hI/JzA39iph - HjP/dqFg0WLxr9eNb7BLPEu9b32WmZfb3rmu0KzUvbd//3214jeL3/5Yu/zB4MMqjL8C - lQt1Gk0H7RIdB70eA2F/EeMQ0xaLBKstWwL7TY5UTiyXDqwt/sPziLeGL5nfXUBTkFtw - Q2hEuEokQVRQtB15DLl2IEVMTKxH3FH8D4ksSZTkgJS71FfpPBkDmQ3UFVkL2S9y19B2 - 8gj5egUXRQbFdiXcQfaDdzAkZUnl5yoZqjqqv6tVqTtqMGr0HgrRlNB8fjhDS0ebRntI - J1v3uJ6g3qJ+tUGgIcbwi1HvkWRjMxNWkynTRrN0c28LXUsBy82jj61qrZNt3I8dsuWy - Xbcbsq88nukQccLD0cxJ2VnIZZ/L+skZmDN17nkecZ6+XsewKBwCN+Pd6JOK9/DV8GP1 - e+d/JyA/MIhgTBQhbgX1B2eFOJEkSZ/IPaFpYfbhyPC1iM7I5CjraMHodzFDp+pis+KC - 4+0SVBN5E7+efpnUm1x+Jumsd4ppqmzqfFpeunkGyGg7F5CJzHyWlZGtn711vu6CZw5v - zujFjFzzPPq8wUtn840KqAv6CxMu68KM6i6OuaJ55UtJSymhTLpssbz4qkMFW8UwzCrd - qu1rLdXE6zLXX9ek1yrXzt5IqpOrm7x5ql6ifqwhrFG48X4TsZmnubcloJW3daAtuF24 - fagjvFOs8/GtmC6Zrsnbid3y3S96Mnq1e9fvlPXZ3qW+29bvc4/v3tBAzH30/VcPsgf1 - Yfy1qCKpW2k29qHpfOkrGRYYhZgcmXNZnrAxsZtyJHPiucy40TwsPJ94n/K18xcIxAi6 - CekJi4vQiqyIDiMjD0gdeCqWKK4o/lIiXVJDclnqorS+9LpMMcoc9UX2upwjmh59S95P - gV9hSDFKCaX04mA6RhOzqnxZxVyVRXVcLQ/GXUhj/lCFps9h2cMftJq0Q3XUdb7p9uoV - 6IcYWBhKGP5lNH2kyTjdBGeqbcZrtmE+bFFhGXuUbOVn7WHjcOyoraGdpr3ScWkH4ROc - jvudgNNn5w8usycfura7Vbpf8Ij3JHg5Y01xat7iPuw+O/g13xd+E/4jAQOB3YQ2Yn3Q - teDSkALSeXJa6Omw6HByRGBkYBQhmhgTdCooNjguJJ6UQEoknw5NCk2GxenZiBTXVMM0 - VDpr+mbG7Lm7mdVZmdnk804XdHMkLzJe/JQ7ldd1qSQ/qQBfaHFZqYiraLt47kp/ybXS - 9LKgcrurJhWalfJVyGtc1XTVX69/qFmsnbwxWHf7Zn19eUNuY2FTaXNlS01rfVtLe2dH - T2f/rcGusdsT3dM9L3sX73y/y9uvfM9qwO9+8oPSwdsPnw19GmEalR4zfOT2OOpJ0XjP - xPwkYkr8mfG07/P0mboXj2Y35jheqcw7LES+zlmqW36wsrD69QPXuuInsz+wm7Gf87eb - vo38+XZnBwCyjzJmlxEQzNsA0C1BUoDEwAT/H24e2NnZ2YIZ4r6z8yc3QAiF/w3aNMoZ - CmVuZHN0cmVhbQplbmRvYmoKNDMgMCBvYmoKMjYzNAplbmRvYmoKMjEgMCBvYmoKWyAv - SUNDQmFzZWQgNDIgMCBSIF0KZW5kb2JqCjQ0IDAgb2JqCjw8IC9MZW5ndGggNDUgMCBS - IC9OIDEgL0FsdGVybmF0ZSAvRGV2aWNlR3JheSAvRmlsdGVyIC9GbGF0ZURlY29kZSA+ - PgpzdHJlYW0KeAGFUk9IFFEc/s02EoSIQYV4iHcKCZUprKyg2nZ1WZVtW5XSohhn37qj - szPTm9k1xZMEXaI8dQ+iY3Ts0KGbl6LArEvXIKkgCDx16PvN7OoohG95O9/7/f1+33tE - bZ2m7zspQVRzQ5UrpaduTk2Lgx8pRR3UTlimFfjpYnGMseu5kr+719Zn0tiy3se1dvv2 - PbWVZWAh6i22txD6IZFmAB+ZnyhlgLPAHZav2D4BPFgOrBrwI6IDD5q5MNPRnHSlsi2R - U+aiKCqvYjtJrvv5uca+i7WJg/5cj2bWjr2z6qrRTNS090ShvA+uRBnPX1T2bDUUpw3j - nEhDGinyrtXfK0zHEZErEEoGUjVkuZ9qTp114HUYu126k+P49hClPslgqIm16bKZHYV9 - AHYqy+wQ8AXo8bJiD+eBe2H/W1HDk8AnYT9kh3nWrR/2F65T4HuEPTXgzhSuxfHaih9e - LQFD91QjaIxzTcTT1zlzpIjvMdQZmPdGOaYLMXeWqhM3gDthH1mqZgqxXfuu6iXuewJ3 - 0+M70Zs5C1ygHElysRXZFNA8CVgUfYuwSQ48Ps4eVeB3qJjAHLmJ3M0o9x7VERtno1KB - VnqNV8ZP47nxxfhlbBjPgH6sdtd7fP/p4xV117Y+PPmNetw5rr2dG1VhVnFlC93/xzKE - j9knOabB06FZWGvYduQPmsxMsAwoxH8FPpf6khNV3NXu7bhFEsxQPixsJbpLVG4p1Oo9 - g0qsHCvYAHZwksQsWhy4U2u6OXh32CJ6bflNV7Lrhv769nr72vIebcqoKSgTzbNEZpSx - W6Pk3Xjb/WaREZ84Or7nvYpayf5JRRA/hTlaKvIUVfRWUNbEb2cOfhu2flw/pef1Qf08 - CT2tn9Gv6KMRvgx0Sc/Cc1Efo0nwsGkh4hKgioMz1E5UY40D4inx8rRbZJH9D0AZ/WYK - ZW5kc3RyZWFtCmVuZG9iago0NSAwIG9iago3MDQKZW5kb2JqCjE4IDAgb2JqClsgL0lD - Q0Jhc2VkIDQ0IDAgUiBdCmVuZG9iago0NiAwIG9iago8PCAvTGVuZ3RoIDQ3IDAgUiAv - TiAzIC9BbHRlcm5hdGUgL0RldmljZVJHQiAvRmlsdGVyIC9GbGF0ZURlY29kZSA+Pgpz - dHJlYW0KeAHtV4k71NsbP2MJ2fedyT6MpbEmO9lDSJasM4xtBjN22aJsoUSILCGR7FtC - kpKtIpIklCRX0b0/SnWve4bfffo9v+f2H3S+z3vO57zvOe8533nf5/t5BwBWnL6NjSUN - AIBADCXZmhggHZ2ckXQzgB7QAgbYs3tiycGUNXDJT9rWM4CgmJ6iTeyPGtw3Hah7t0nN - kj3ayKe5XpT8k03/qFlw3mQsAAg0VHiQ4OEAUHFBzIXfwzIU7LWHtSiYZG9rCNfYQGHC - 72KakxTstYv3+VJwRGhwKAD0UAAXNphEwcUQn/UK2NWnUvThWDzUM0gAsI8D6+uJA4BV - A+rROAIRYsQ5iG1xBBwFL0CMIQSGwXvuNsrbMnkTj9vB0QiKEDAFYcAbRAEbYAusARIY - giAQCIUEsSWcGcFRBvZqQBUiE4ABClCQQB8oQaQEH9RP/Kvt+ncE8tCzHwiFpyCBGSAC - LNxH8Rmx+yiASHjWnl0B2oIAAaBgTP/9zjy7Pv/tjkoAUOIP7bAR+QEwoPx2PD90zr0A - ZMKYCKJ/6OS/A6DkBUDDO2wYKXxvL+KfgRrQAUbADvjgbVFABb6xFXCF908AuaAG9IHn - 4HcEM0IWYYbwQ2QgGhCTiG9UElTWVDFUNVQz1PuptalJ1DXUr2mEaJxo8mmmaflpXWmv - 0q7uU94Xt2+EToAugK6XnpPen/4egzBDNMPM/kP7ixkRjHjGJ0yaTNXMPMxnmbdZiCxv - Wd1YZ9gc2KbYHdhnONw4ljmDOL9xpXHzc9fy6PJM8RL46Pkq+Q35lwRSBNGCk0Jxwijh - aZEUUU3Rj8iqA25i/GLT4pckHCWFJN9I3ZAOlzFEcaCWZDvkstA+8noKwgrfFV8p3T1Y - jTmvHK2CV3VQM1XX0sAcktWUPCymdUBbXEdaV0FPTV/fwNrQ3SjkyBnjYpM203GzdQtW - S8zRE1bx1jU2U7Y0dqr2vsdLHKYd2Z2snLNcnrhyubm4V3pseOlgs3GvfdTx2b7v/Y0D - qgh0xICgiZDDpOpQ7rAz4V8iiVHLMR6n5uJc4+cTsadXk8lndlKy0kTTm8+ZZi5mn7rA - n3Mr9+QlRH5NoV0RKG4o8SzjLh+tOFtlUI243l97ps6ynrthoam+Jb7NtkPmFuia7e7s - LeiL6fcYMH2AeYgcZh1FjG09/ji+8vTt1NL08szq7Ke5bwv0r3mXZJa1V+xXCWtpH69v - DP/+cZP7s862/7fCP4d3dn7lwq9c+JULv74L//9d+MEbW1P/rRvkfuhANCSOOCi+P+Eu - f2gT2uXXYMi3FM7DA1/IixQuxEKGQYL/5Uo0nGN2+fUgZNA9pL7LnPqQnwOhlcKqex7I - uzNvQIYcSwLhsMfBlWCvTtijM0CNgOUFrAgIVDM0xrTNdKL0GQxfGHFM4yzarLXsPBxJ - nOvcJ3k8efF8gfwkgQjBWKHTwqkimaIXkYUHSsQqxWskGiQvScVKe8tYopRlBeWo5N6j - x+U7FUoVU5SIB49jtJWlVNhUvqq+URtT79AoO5SuST7sqmWiraQjoEuj+0FvTf83g/eG - K0bvjrw1XjJ5bbpotmD+ymLO8uXRF1Yz1qs2X20Z7YTs0ce1HaxOuDkGOSU4n3cpP9ns - 2u/21H3BY81zG0uL4/AW8ZHDq/sa+dn4uwb4B4YTkojZQVXBt0PGScvk72Ec4TIROpF2 - UX7R8TF5p27E9sU9i59IeJw4cnow6V5y35nus50pramNaTfTazKqzpVnlmSdzvY5b35B - MYcz5/PFl7l9eZWX0vIJBccKVS8LXP6r6GVx15X8krBSuzKlcpbytavDFdWVZ6qw1/Sr - Raq/X5+r6am9ciOuzu2mbr1o/U7DQmNfU3lzYotXq0GbWDtV+2JHX2fprbgu19ta3YLd - X3tmejvv5PeF33XoV71Hd29moO5+3AObQfHBzYeDQ4XDgSM6o2yjb8baHqU8NnpC9eTO - +KmJQxNbT1smg6fkp1afVU97Pxd7Pj9T/MJ6FjHb+hI/JzA39iphHjP/dqFg0WLxr9eN - b7BLPEu9b32WmZfb3rmu0KzUvbd//3214jeL3/5Yu/zB4MMqjL8ClQt1Gk0H7RIdB70e - A2F/EeMQ0xaLBKstWwL7TY5UTiyXDqwt/sPziLeGL5nfXUBTkFtwQ2hEuEokQVRQtB15 - DLl2IEVMTKxH3FH8D4ksSZTkgJS71FfpPBkDmQ3UFVkL2S9y19B28gj5egUXRQbFdiXc - QfaDdzAkZUnl5yoZqjqqv6tVqTtqMGr0HgrRlNB8fjhDS0ebRntIJ1v3uJ6g3qJ+tUGg - Icbwi1HvkWRjMxNWkynTRrN0c28LXUsBy82jj61qrZNt3I8dsuWyXbcbsq88nukQccLD - 0cxJ2VnIZZ/L+skZmDN17nkecZ6+XsewKBwCN+Pd6JOK9/DV8GP1e+d/JyA/MIhgTBQh - bgX1B2eFOJEkSZ/IPaFpYfbhyPC1iM7I5CjraMHodzFDp+pis+KC4+0SVBN5E7+efpnU - m1x+Jumsd4ppqmzqfFpeunkGyGg7F5CJzHyWlZGtn711vu6CZw5vzujFjFzzPPq8wUtn - 840KqAv6CxMu68KM6i6OuaJ55UtJSymhTLpssbz4qkMFW8UwzCrdqu1rLdXE6zLXX9ek - 1yrXzt5IqpOrm7x5ql6ifqwhrFG48X4TsZmnubcloJW3daAtuF24fagjvFOs8/GtmC6Z - rsnbid3y3S96Mnq1e9fvlPXZ3qW+29bvc4/v3tBAzH30/VcPsgf1Yfy1qCKpW2k29qHp - fOkrGRYYhZgcmXNZnrAxsZtyJHPiucy40TwsPJ94n/K18xcIxAi6CekJi4vQiqyIDiMj - D0gdeCqWKK4o/lIiXVJDclnqorS+9LpMMcoc9UX2upwjmh59S95PgV9hSDFKCaX04mA6 - RhOzqnxZxVyVRXVcLQ/GXUhj/lCFps9h2cMftJq0Q3XUdb7p9uoV6IcYWBhKGP5lNH2k - yTjdBGeqbcZrtmE+bFFhGXuUbOVn7WHjcOyoraGdpr3ScWkH4ROcjvudgNNn5w8usycf - ura7Vbpf8Ij3JHg5Y01xat7iPuw+O/g13xd+E/4jAQOB3YQ2Yn3QteDSkALSeXJa6Omw - 6HByRGBkYBQhmhgTdCooNjguJJ6UQEoknw5NCk2GxenZiBTXVMM0VDpr+mbG7Lm7mdVZ - mdnk804XdHMkLzJe/JQ7ldd1qSQ/qQBfaHFZqYiraLt47kp/ybXS9LKgcrurJhWalfJV - yGtc1XTVX69/qFmsnbwxWHf7Zn19eUNuY2FTaXNlS01rfVtLe2dHT2f/rcGusdsT3dM9 - L3sX73y/y9uvfM9qwO9+8oPSwdsPnw19GmEalR4zfOT2OOpJ0XjPxPwkYkr8mfG07/P0 - mboXj2Y35jheqcw7LES+zlmqW36wsrD69QPXuuInsz+wm7Gf87ebvo38+XZnBwCyjzJm - lxEQzNsA0C1BUoDEwAT/H24e2NnZ2YIZ4r6z8yc3QAiF/w3aNMoZCmVuZHN0cmVhbQpl - bmRvYmoKNDcgMCBvYmoKMjYzNAplbmRvYmoKMzMgMCBvYmoKWyAvSUNDQmFzZWQgNDYg - MCBSIF0KZW5kb2JqCjQ4IDAgb2JqCjw8IC9MZW5ndGggNDkgMCBSIC9OIDMgL0FsdGVy - bmF0ZSAvRGV2aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae1X - iTvU2xs/YwnZ953JPoylsSY72UNIlqwzjG0GM3bZomyhRIgsIZHsW0KSkq0ikiSUJFfR - vT9Kda97ht99+j2/5/YfdL7Pe87nvO857znfed/n+3kHAFacvo2NJQ0AgEAMJdmaGCAd - nZyRdDOAHtACBtize2LJwZQ1cMlP2tYzgKCYnqJN7I8a3DcdqHu3Sc2SPdrIp7lelPyT - Tf+oWXDeZCwACDRUeJDg4QBQcUHMhd/DMhTstYe1KJhkb2sI19hAYcLvYpqTFOy1i/f5 - UnBEaHAoAPRQABc2mETBxRCf9QrY1adS9OFYPNQzSACwjwPr64kDgFUD6tE4AhFixDmI - bXEEHAUvQIwhBIbBe+42ytsyeROP28HRCIoQMAVhwBtEARtgC6wBEhiCIBAIhQSxJZwZ - wVEG9mpAFSITgAEKUJBAHyhBpAQf1E/8q+36dwTy0LMfCIWnIIEZIAIs3EfxGbH7KIBI - eNaeXQHaggABoGBM//3OPLs+/+2OSgBQ4g/tsBH5ATCg/HY8P3TOvQBkwpgIon/o5L8D - oOQFQMM7bBgpfG8v4p+BGtABRsAO+OBtUUAFvrEVcIX3TwC5oAb0gefgdwQzQhZhhvBD - ZCAaEJOIb1QSVNZUMVQ1VDPU+6m1qUnUNdSvaYRonGjyaaZp+Wldaa/Sru5T3he3b4RO - gC6Arpeek96f/h6DMEM0w8z+Q/uLGRGMeMYnTJpM1cw8zGeZt1mILG9Z3Vhn2BzYptgd - 2Gc43DiWOYM4v3GlcfNz1/Lo8kzxEvjo+Sr5DfmXBFIE0YKTQnHCKOFpkRRRTdGPyKoD - bmL8YtPilyQcJYUk30jdkA6XMURxoJZkO+Sy0D7yegrCCt8VXyndPViNOa8crYJXdVAz - VdfSwByS1ZQ8LKZ1QFtcR1pXQU9NX9/A2tDdKOTIGeNikzbTcbN1C1ZLzNETVvHWNTZT - tjR2qva+x0scph3Znaycs1yeuHK5ubhXemx46WCzca991PHZvu/9jQOqCHTEgKCJkMOk - 6lDusDPhXyKJUcsxHqfm4lzj5xOxp1eTyWd2UrLSRNObz5lmLmafusCfcyv35CVEfk2h - XREobijxLOMuH604W2VQjbjeX3umzrKeu2Ghqb4lvs22Q+YW6Jrt7uwt6Ivp9xgwfYB5 - iBxmHUWMbT3+OL7y9O3U0vTyzOrsp7lvC/SveZdklrVX7FcJa2kfr28M//5xk/uzzrb/ - t8I/h3d2fuXCr1z4lQu/vgv//134wRtbU/+tG+R+6EA0JI44KL4/4S5/aBPa5ddgyLcU - zsMDX8iLFC7EQoZBgv/lSjScY3b59SBk0D2kvsuc+pCfA6GVwqp7Hsi7M29AhhxLAuGw - x8GVYK9O2KMzQI2A5QWsCAhUMzTGtM10ovQZDF8YcUzjLNqstew8HEmc69wneTx58XyB - /CSBCMFYodPCqSKZoheRhQdKxCrFayQaJC9JxUp7y1iilGUF5ajk3qPH5TsVShVTlIgH - j2O0laVU2FS+qr5RG1Pv0Cg7lK5JPuyqZaKtpCOgS6P7QW9N/zeD94YrRu+OvDVeMnlt - umi2YP7KYs7y5dEXVjPWqzZfbRnthOzRx7UdrE64OQY5JTifdyk/2eza7/bUfcFjzXMb - S4vj8BbxkcOr+xr52fi7BvgHhhOSiNlBVcG3Q8ZJy+TvYRzhMhE6kXZRftHxMXmnbsT2 - xT2Ln0h4nDhyejDpXnLfme6znSmtqY1pN9NrMqrOlWeWZJ3O9jlvfkExhzPn88WXuX15 - lZfS8gkFxwpVLwtc/qvoZXHXlfySsFK7MqVylvK1q8MV1ZVnqrDX9KtFqr9fn6vpqb1y - I67O7aZuvWj9TsNCY19TeXNii1erQZtYO1X7YkdfZ+mtuC7X21rdgt1fe2Z6O+/k94Xf - dehXvUd3b2ag7n7cA5tB8cHNh4NDhcOBIzqjbKNvxtoepTw2ekL15M74qYlDE1tPWyaD - p+SnVp9VT3s/F3s+P1P8wnoWMdv6Ej8nMDf2KmEeM/92oWDRYvGv141vsEs8S71vfZaZ - l9veua7QrNS9t3//fbXiN4vf/li7/MHgwyqMvwKVC3UaTQftEh0HvR4DYX8R4xDTFosE - qy1bAvtNjlROLJcOrC3+w/OIt4Yvmd9dQFOQW3BDaES4SiRBVFC0HXkMuXYgRUxMrEfc - UfwPiSxJlOSAlLvUV+k8GQOZDdQVWQvZL3LX0HbyCPl6BRdFBsV2JdxB9oN3MCRlSeXn - KhmqOqq/q1WpO2owavQeCtGU0Hx+OENLR5tGe0gnW/e4nqDeon61QaAhxvCLUe+RZGMz - E1aTKdNGs3RzbwtdSwHLzaOPrWqtk23cjx2y5bJdtxuyrzye6RBxwsPRzEnZWchln8v6 - yRmYM3XueR5xnr5ex7AoHAI3493ok4r38NXwY/V7538nID8wiGBMFCFuBfUHZ4U4kSRJ - n8g9oWlh9uHI8LWIzsjkKOtoweh3MUOn6mKz4oLj7RJUE3kTv55+mdSbXH4m6ax3immq - bOp8Wl66eQbIaDsXkInMfJaVka2fvXW+7oJnDm/O6MWMXPM8+rzBS2fzjQqoC/oLEy7r - wozqLo65onnlS0lLKaFMumyxvPiqQwVbxTDMKt2q7Wst1cTrMtdf16TXKtfO3kiqk6ub - vHmqXqJ+rCGsUbjxfhOxmae5tyWglbd1oC24Xbh9qCO8U6zz8a2YLpmuyduJ3fLdL3oy - erV71++U9dnepb7b1u9zj+/e0EDMffT9Vw+yB/Vh/LWoIqlbaTb2oel86SsZFhiFmByZ - c1mesDGxm3Ikc+K5zLjRPCw8n3if8rXzFwjECLoJ6QmLi9CKrIgOIyMPSB14KpYorij+ - UiJdUkNyWeqitL70ukwxyhz1Rfa6nCOaHn1L3k+BX2FIMUoJpfTiYDpGE7OqfFnFXJVF - dVwtD8ZdSGP+UIWmz2HZwx+0mrRDddR1vun26hXohxhYGEoY/mU0faTJON0EZ6ptxmu2 - YT5sUWEZe5Rs5WftYeNw7KitoZ2mvdJxaQfhE5yO+52A02fnDy6zJx+6trtVul/wiPck - eDljTXFq3uI+7D47+DXfF34T/iMBA4HdhDZifdC14NKQAtJ5clro6bDocHJEYGRgFCGa - GBN0Kig2OC4knpRASiSfDk0KTYbF6dmIFNdUwzRUOmv6ZsbsubuZ1VmZ2eTzThd0cyQv - Ml78lDuV13WpJD+pAF9ocVmpiKtou3juSn/JtdL0sqByu6smFZqV8lXIa1zVdNVfr3+o - WaydvDFYd/tmfX15Q25jYVNpc2VLTWt9W0t7Z0dPZ/+twa6x2xPd0z0vexfvfL/L2698 - z2rA737yg9LB2w+fDX0aYRqVHjN85PY46knReM/E/CRiSvyZ8bTv8/SZuhePZjfmOF6p - zDssRL7OWapbfrCysPr1A9e64iezP7CbsZ/zt5u+jfz5dmcHALKPMmaXERDM2wDQLUFS - gMTABP8fbh7Y2dnZghnivrPzJzdACIX/Ddo0yhkKZW5kc3RyZWFtCmVuZG9iago0OSAw - IG9iagoyNjM0CmVuZG9iagozMCAwIG9iagpbIC9JQ0NCYXNlZCA0OCAwIFIgXQplbmRv - YmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZXMgL01lZGlhQm94IFswIDAgNzU2IDU1M10g - L0NvdW50IDEgL0tpZHMgWyAyIDAgUiBdID4+CmVuZG9iago1MCAwIG9iago8PCAvVHlw - ZSAvQ2F0YWxvZyAvUGFnZXMgMyAwIFIgL1ZlcnNpb24gLzEuNCA+PgplbmRvYmoKNTEg - MCBvYmoKPDwgL0xlbmd0aCA1MiAwIFIgL0xlbmd0aDEgNzE3MiAvRmlsdGVyIC9GbGF0 - ZURlY29kZSA+PgpzdHJlYW0KeAG9WXtYlNW6f9f3fXNBUG4Kw3Vm+BiugwgooJiMOMNF - TFEUGfMygCCSGClSlhK7dJd4OVtNbWtHs4s7JXME0gG2Ru7c6a6dVrub2043szpPPtU5 - dWqHzJzf+mYk7dn1+EdPfM87613X97d+77vW960FMSIKoDYSyVLTWNVEa2gAJa9AWmta - mg2bP5+0l4hNIxKX1TUtaQz+4C9/I5JcRMMClixbXff11rzpRCNeJNIa6murFn+jX9ZN - FHYJ/bPrURAwMOIOovBo5OPrG5vvtm1UP4O8BXnLsjtqqkJygxzItyEf11h1d5N25bDv - kX8SecPyqsbahPzouchjfEppumNlM3tGqEP+K+QLmlbUNv35geUZRLqxwHcOZQwP/wsg - Na1AaqOxvhKlWPkRflSv08TrdK8qIVFB1BANRAsh8qNh5I/xhys5TN2XBqJxPwWpTlCS - qo0ipXTSE3nehVzgqXuO57LqJQpyN3q+FvPQp4eL4M6fSP20mfbQEdh5GnoSLaRH6Cxr - oB42n7rpLRZLo6mNJHLRNHqFeTyvUR09ifbNdIp20FFgSaJGGoXaLczkuQd5C/RqWud5 - nOIpl35PJ2g8Rt1CVzwHPV2onUVz6BB1oP/LTBaOSqGeZz2XML+ZGHMdal7zTPMcoRAy - UwGVoXQdnWQm8YKnnnSUB3SP0j7aTy/QF+x+1u2p97R4zns+JAG10VSOZy3rZh+KR6Tf - ex71/LfHDSaSKAVWHbSdnsD4R/D0w1U2djtrZtvZDsEi3C90S+tV4e5B8JBMRXiK6Q56 - CAz00Iv0P/Qv9qWgE4PEZvG0Z5znf+GDUsySz6SWWvA8iGcL5tTH1GwMm8LK2Fr2MNvB - 3hBShDlCpXCXcLdwWZwuzhdXi29IK6VO1SbVI2p/97eePs9LnjcpnGLoNsRMK2Z3is7T - N/QDEzFWNDOxPFbAFuJpY3uEHraf9QhlrJ+dFw6x99nH7Es2IKiEAGGUkCo0C9uFDuGU - 8Kq4VNwh/lF8X/xWmqQSVPtVn6hNmn+6q90b3K968jwfer7HitOSEZ4poOm0iKow2yZE - 632YxWE8R+C1F+k0nVWej1k0XaHvwQKxEBbJMtmteKazGayOLWV7WS+ekwqW/xPgCMFP - CBbChWihXKgWGoU24U2hTYwSU8Sp4jzxCJ4z4lvigDggqaRQaZRUJJXQJqlR2o3ngPS0 - 1CmdU41XTVJNV1Wo2lQbVJvEGtVrqrfUreot6k71l+qvNEmaaZo7NJvgnbOI2Rd8a8Cb - SCwe6DNpOdUwK6umnfDGflZF7Yiuxewh8NVESZ4FYqtYJIxBNJykexGtu2ktbRDn037P - O+IhehuRsgzDtdGfpAKKUe2Cd+6nMYgi32NJTklOSkwwxctxRoM+NiY6KjJCFx42amRo - SHDQ8AD/YX5ajVoliQIjs00udBicCQ6nlCAXF6fxvFyFgqrrChxOA4oKb2zjNPB+Vai6 - oaUFLet+0tLibWkZasmCDBNpYprZYJMNzr9bZYOLzZtZCX2zVbYbnFcU/VZF/4OiD4du - NKKDwaartxqczGGwOQtb6tttDmuamfVYQMewNDPfOCzkzwd20pSqtfU6JLyFzRkpW23O - CBk66kSTrWqxs2xmpc0aZTTaUYaiWZWwkWZe6gRO2hiwWF680WWhagfXquZXOsUqu1Nw - 8LGCU53hstUZfs8nuh+z1zTbpusqnYKpsKq2vdBpcWwEuTzr4LmqTciVlhswrLDeXulk - 630gOMYGIOVwa2Ubx+VoMDj95AK5vr3BAXJpVmVnpCXSJldZ7U4qq+yMsEQomTRzj641 - z4jZ96RNTpvM0zyjrtWbfvqAt/z1fp7qWl/8AGnprCECGLcklwCn01CjGJEBNpf/1OZS - e00ueMKfnWGaS4FnilNAzIgmp8pUUuVsK78Go97qBedosHb6RUTyOTgK7GjvaA+aAE+h - fZBsaP+W4EL5yhc3llT5StSmoG+JV3JHD8WKk1Vd01sUYjDrep1cz/3bovgUeVlnu64A - eU4Nx+wc6cwsLas0Og12FLgo1VzqIr+yyqOMbbG7mGe9i6wxPXiDiYsWotrMQ22pFfaR - STOjIMUIbbTZUIhZF/JYMbQb2ksWtxsKDfUIJsmkpKiobbeng8HySvBEs2HRYo8aUmvt - 9gkYJ52Pgy5o3m7HCA2+EZAqRemDaDTGXAqvJJRVzqx0tlmjnBarHV5A+PaXVTr7Ebl2 - O1plDCEF4rVLdT7MmcCckYL6LO8o5RgDQ9jb2/mY5ZWy0dnf3h7VztebN+9i9NMCi6/A - RbwJJm5zsbYy9EUiG6N4gWyUjYBl55yORUhfiygXjftlhrOHcKNnDtBmKwzn/koMj78Z - hifcFMN5Q0hvYHgiMOdxhm/57RiedAPD+b/MsGUIN0BOBlqLwnDBr8TwlJth2HpTDNuG - kN7AcCEw2zjDRb8dw8U3MFzyywxPHcINkKVAO1VheNqvxPCtN8Pw9JtieMYQ0hsYLgPm - GZzhmb8dw7NuYLj8lxmePYQbIOcA7WyF4YpfieG5N8Nw5U0xbB9CegPD84DZzhm+7bdj - eP51DOODtwBn0vM4e4k4qeW7qDzVRdp0vPwg2iAcVs9DeB66eNFFEoSgay5Sr3K2q0jt - xSgqqkgdk5EVbAxOhBRIW1xXP1Kd+GGKS7p1oAufXwzftaQOgx1/0nMr6M3Pg7w3Q3+e - qnB+4aMwo8Yo+oR9KqUnXt2+UEyNv/pmg7jGNHBKdaLbXXDIPQID8nF34Yg5A+OG0nhl - XC9cEZBVEH9ISLoXIQWHjO+FTZxMFW24T+MWQ5mRyaGTWA6TRc0IphFldo7F7BU6WKT7 - zAmXX0bEYMXpfcP8U/xdJ1UnBhKkCz9MEWvSzt81kCy9nZb93tir/wksKZjjbGWOYBFz - BGWwr4YoFAIHPxWLinV/nzYmwyj7saxQluXHZMYivxKedXf8y8O+uDJ4L6v9zv2N8LXw - yuCrQubg2MFAYT56CdTkuYjzRgkF4kyZ52MzkcYpLOpxHquA+cTrnMf1lPMQIBkHfTT0 - 0eljMkyZOdn5bAQLZGoNnjCWnYMnQY5DTs6Oz8oMD9OI6rCszOwckCLHJSbk8CQhhxN1 - eVHNU/GxpuVZTbU5C8KCF7Euiz7Yb+SKezaXpkQ9nc50T5yoqzM8oA40BehDYsxpCQui - A1VFl9bs2BVjeG/PKnPJga2jotUjhkenL5k+TxipNevS5pdPSyn/657i4kcGd0XHieL6 - AHWBbClueO6hHU+Ggt9Vng+ltdJ0iqRE36z9cdbmsaPDKZ/PWofZMYRoCNIRF+FZ2Tcd - 7yyy1JIcJ+SEUFZmmLTkiKqi9ZnlRXHyvG1Nj2UeKXVf7nu9J2Mim/OP504IL9U88HTj - Y/svbrjrzdMs6zJOjhOcnPu1ngs46RXhvB4/FMlaGqmgiMSplKOJUdDAepgmTGPkpsFr - FsIKJMN+qGJfzE4E02qN9DuTislXv4xdsmvzkony0ZGNeTX32WadeSc3h83/aEX/3SMi - Rh9e86osPjhz2dTHnzi9ILsob+vosugghIuaCazgdvfWVYX3d7UjNIDP7M6TzuLkp6e0 - IXzBWGscVywZlDQRfCkrLSw8J0sEKCN8mqUO9wJVXKxg1PBA8MHPThC7zQkxB86lztnn - Pnv45VHHBf2YB84tyjUXHVz77Gu3jGdFva33nbx9giHx9jWnmidHp66RJHnKg1czX2m5 - sOep4sSJ2yrem1X2HYthw9nofZ2Ldj934kjNupf6gXkdgK/EuuF7UKhv5QjKavHtCFlY - j1kamdUd/+g4yz1uPi6lDLylOvEKYmID+q5S+gb6evK1JiC6WRYYeqHbfaab70R8r4Ad - 9QX4LoE3UdZnCJQoiAnW1Ir3sF4RO8OxQoxIw5GGK2NpsDL4ggifxLzrQg7loaXGVhHq - 8yRALuk0zMyru7NtcvyoGV2176TpYnf17Q2bd2vDcXnd8YfDAyOa6s6a7+6W0h+ZEX9L - fnxhRfmjs7cM5gif3V625cDgVqGvMbN077nBM9yXCl5pP/BGULgP7zBg1SnMBHk9mKW5 - Dg8PKHjMu38t6TA4+uovjY6M23b8P0YFRbVazDMKc7PC7uLWF87aN/fxwZnCE9UTFw8P - Kxh359JBfgkIX6zwvCudxxoLQIx4rfI9k1vrpbBrce2bMA+NkByBjL41FSJeMESn9T71 - ckJ87RNdz3+Q4/6z+7v3Xhw3gVV8eu5jIXnnwoevdnZcYoEd7kH3syz1KvYei/sLxW6U - e470Ova0ERRHeCEq3jFipqMUm73AEw0MeAHBK0EXexHJ0RSAnRR+VtDwSOUBnI1AUVwV - Igp8tSUmJIqy+EFUiKG3r3GCMTI0rrf1H4NPHYm1ldTfe+xUztS3H9q9uigltblbiG2b - f7Rv8e41cw+8IfzXlpKkie7PgfPxnYvGxZYMvgd/PO/5UvhCNQ/MXNvfg4GQ+fYejkx5 - RSIdhbgRkYaf53EoInZFLzrvRpqQEyrnZLGXj1k69B07AuJCM4bHjoo12hJb88N2bdVv - Vc1zv7l90JYb6s+ELX7a3y0RTm+HHbwzpC4pHem462L92jvGD1gERLH37SYNaVqfxpGE - snC8ZkT+pmGfH2AlF9zJTHX5OffBS+yKlO5+kK1WDQ4M/pNtcy8XTEoM8qgg/+qLf1oU - OPFbCvZe5f71fPD7vFxJ/d15yhtYwC7DeCn+kKqT3cm4Tmbft18dF7BNS4x/B/z4F6QK - oQJVBR1RH6JdSFOkldQkEa1CuhZiZi/ROsgG1K9DnssKSJQwnp5HO/7+HEsOOkAXWAV7 - hH0mFAgOoQX3h1rRKu4U35KC0ILjCcL9oEAN2FsE6EG0AF8Mnw0LgNd4LcMbxItaDb9S - 5cySwvK5qcW1y1pqm5fWVKXNqF629M5VtWgp4Db6G0gt7k3/3R+3l4SbtjyyUiHuYKfi - lnWGcgs8Cze7c3FHig8C/n1VAsmHjIOkpk7WURs7QH+APAYRaSnbSKshGyB/hEhD2kHk - etjGTklr6WWrKZJNtfhL+tkjI/S6Yf76111M3b1X/67u4z4WgVv2D1lE53DymzyMPcb2 - 0WLSs6fIxO4BsiS2uyt5md6BqoPUBGmDiMovYwc7YzP1J5mZTBJDnwSKldgx/acZafpP - MlwC69SfSnRJSF6IRc4SqO+P2at/PmaJ/iSkw1t1KBktjukPxizTb491sd2d+m0xLoY+ - W73Jqhh0PaZvTN6pX5yh1E/b6RI6OvXjUV9h8ddn5xr142Iu6dMTXVqGfFrMNH1Kxt/1 - 8eiIZgYMarIE66NjtusnoCo2xpY4AdLHDrE9lML2dJqm6nuhYrpdJcm5O13s3q7ipAyT - i91jyS5O2plcnGhKnqY3JRcmJkKvOKNZp7lNM1mTqUnFBW2CxqiJ0ozUhmiDtCO0Adph - Wq1W42LPdObr1X2sg/JBS0eXVq1VudizKJT62GGl8PBxraQVtKQd6fJ8gH/mMBrpYh3d - CAxGUI6pFU3tYoexFnjRYYseoYwNRKkIQoTxj2H+SwLTCgghJ9vsUtP6sJZ8XX7IpODx - hdaf+3EoNdd+U3/+T8dinDtxF+M8FGPHtRcUT4z9WnPdNeVn0+ZVqKotSE0tnbW6q6Wp - oU65xpNttQ7c5jk3tuBata3aYDja0OS7o0xwVNfU83ukqlpnk1xrdTbIVsPRFqUfL76u - uo5Xt8jWo1Rnm115tM5Sa+1ssbTY+HVmV3XBigU32NowZGtFwb+xVcAHW8FtVSv9fmJr - Aa+u5rYWcFsLuK1qS7Vii0/etrS8YGUzohNXfbhqSyp3lsycV4kbbbvVxQ7w+79V9P+8 - rYdICmVuZHN0cmVhbQplbmRvYmoKNTIgMCBvYmoKNDM3MQplbmRvYmoKNTMgMCBvYmoK - PDwgL1R5cGUgL0ZvbnREZXNjcmlwdG9yIC9Bc2NlbnQgNzcwIC9DYXBIZWlnaHQgNzI3 - IC9EZXNjZW50IC0yMzAgL0ZsYWdzIDk2Ci9Gb250QkJveCBbLTkzMyAtNDgxIDE1NzEg - MTEzOF0gL0ZvbnROYW1lIC9YUUlGU1crSGVsdmV0aWNhLU9ibGlxdWUgL0l0YWxpY0Fu - Z2xlCi0xMiAvU3RlbVYgMCAvTWF4V2lkdGggMTUwMCAvWEhlaWdodCA1MzEgL0ZvbnRG - aWxlMiA1MSAwIFIgPj4KZW5kb2JqCjU0IDAgb2JqClsgNjY3IDAgMCAwIDAgMCAwIDAg - ODMzIDAgMCAwIDAgMCAwIDAgMCAwIDAgNjY3IDAgMCAwIDAgMCAwIDAgMCA1NTYgMCA1 - MDAKMCA1NTYgMCA1NTYgMCAyMjIgMCAwIDIyMiA4MzMgNTU2IDU1NiA1NTYgMCAwIDAg - Mjc4IDAgMCAwIDUwMCBdCmVuZG9iagoxOSAwIG9iago8PCAvVHlwZSAvRm9udCAvU3Vi - dHlwZSAvVHJ1ZVR5cGUgL0Jhc2VGb250IC9YUUlGU1crSGVsdmV0aWNhLU9ibGlxdWUg - L0ZvbnREZXNjcmlwdG9yCjUzIDAgUiAvV2lkdGhzIDU0IDAgUiAvRmlyc3RDaGFyIDY5 - IC9MYXN0Q2hhciAxMjAgL0VuY29kaW5nIC9NYWNSb21hbkVuY29kaW5nCj4+CmVuZG9i - ago1NSAwIG9iago8PCAvTGVuZ3RoIDU2IDAgUiAvTGVuZ3RoMSA5OTcyIC9GaWx0ZXIg - L0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ab16e3iU1bX32vu9ziWTmcncM5PJZDIzmdxD - SMiQQMaQhHsMBCFBggkQCAg1YIyiwkGFIhGpQrkIPVS05RKKGUIKAxQ/ykHR1lPxitdW - j2i1T/N4Tg/aVpiZb+13Qgo+/fr5R5/OO/u+373X+q2199qXFwgAaGEdcBBeuKK9C54n - 4zHnFXRrF/Z0Zz7+xfi9AGQaALd8cdeSFYaP/uNXAHwUQK1dsnz14rJtr8wH0J0HMF7u - 7Ghf9Kf/XX4SwHMQ3y/vxAx1llSE6Y8wnd25ovu+RRUqC0AWj+l5y+9a2F4WHVuO6TZM - F69ov69LfkD9V0w/genM77Wv6Jj7wMPbMB3BdFbXXXd3000clmW9ienGrlUdXb945Hsl - AN5spO9VzCP4sJ8WRFiFYR2Mxhyq5F33uOuR4ZAH4Vs5yaSIgQQyqDBUgwbbZL8U0EEq - 6MGAcSOkgQnMSj5yJZwFvXAGcoR14OCLwA2QeBfdeyyM35b4TLgA+viKxP9wlfgGogQn - aby6Cs7C47AH+pHigxjPgfmwC14my+AkmQeD8DbJgEKUDw9RmAavkETiNVgMP8H63XAO - tsNRpCsHViAV02AL8SXux3QY4wtgfeIZyIYK+D6cgRC2ugWGEocSx7B0JtwGfXAY3/81 - 8dKjfFriucRl5HQGtrkeS15LTEv0I3f5UAONmLsetcLHvZfoBBtUInU/gh/DPvgl/JE8 - TAYTnYmexMXEx4iyDZzQhM8aMkg+5vr57yd+lPhDIo5I5EAu9toG2+BZbL8fn7Moqjpy - J+km28h2GqYP00F+g2CNxxCHIEzEZxLcBY8iAifhPPwJ/kq+pDZOz3VzLyTKEv+L8piK - XDJOOqAHn434bEGeThORFJMJpJGsIT8k28kbNJfeRpvpvfQ++hnXwM3jVnNv8HfzA8Jm - YZeoiX+VOJ24kHgLrOCC21Fn1iJ35+AiXIFvCIdtOYmPVJIaMh+fdWQPPUn2kZO0kZwl - F2kf+R35hHxJrlKBaqmZ5tFuuo0epufob7il3HbuKe533Ff8eIEK+4RPRZ/0fnxBfFP8 - N4nKxMeJv+CIk8GDkqmBBrgD2pHbLtTWf0MujuDTj1I7Dy/Ay8rzCXHCEPwFUQBiJA4y - ikzHp4HcShaTpWQvOYXP8wotX1MUBFVRA7VSJ22iC+gKuo6+Rddx6VwuN4Wby/Xj8xL3 - NneVu8oLfBpv5ifyk2Ezv4Lfjc9+/iA/wL8qhITxQoMwW1gnbBI2cwuF14S3xbXiFnFA - /FL8bylHmibdJW1G6byMOvtLZQRc93iSjdSPgu/BQlJLFsAOlMY+0g69qF2LyKOIVxfk - JFq5tdxEWoza8Dw8gNq6G9bAJm4e7Eu8w/XBJdSU5djgOjjA14BL2InSeRiKUYuGn3Aw - N5gT8PuyvVmeTHeGy5nusNusFrMpzWjQp2g1apUsiQLPUQL5dd76tsyIvy3C+72TJhWw - tLcdM9pvyGiLZGJW/c11IpnsvXYsuqlmGGsu/lbNcLJmeKQm0WdWQVVBfmadNzPyn7Xe - zCiZO6MZ44/XelsyI0NKfLoSf0KJp2Dc48EXMutsnbWZEdKWWRep7+nsrWurLcgnJ8MI - h7ogn00cYdCwhiMwoX1Npw0DVqMu4vDW1kXsXoxjGeera18UaZzRXFeb7vG0YB5mzWzG - Pgryl0aQTnhMu8i76LFoGBa0sVj7vOYI194SoW2sLUNexOqtjVjv/9T2t+T1WN3mGwoj - 1Fff3tFbHwm3PYbgsmQbS7VvxtTUpkxslm5oaY6QDcNEMBqXIaWM3A5vHaOrbVlmROWt - 8Xb2LmtDcGFm84Aj7Kjztte2RKCxecAetiuJgvyTtrWVHuT+ZMEtBbewsNJjW5sMf/9I - Mv/1syy0rT3/EYZTZ44AQFhP3slIZyRzodKJF4mtYF5HBfQurECc8NdCkM2lSM+ECEWd - 4XwRwTe5PbKu6ToZnbVJ4tqW1Q6o7A7GQ1tNC9Zv69WPRUlhfb03s/crQBF6h/54c077 - cI7o038FrJAJekRXIqT9erxHAQa57rR5O5l8exSZYtprq7shA9MMGkZzxBQZNbWx2RPJ - bMGMKOTlT42CqrH5KCFbWqIksSEKta6TaM+4O+ZjcT5TtaW12D8mCvIxI9eDscL8zHrk - up7pSmZvZu/kRb2Z9ZmdqEy8TwmxoKO3pQgRbGpGnGAW9hhuSR+JdrS0jMV2ilg7+ApW - 723BFpYNt4ChklUUw0rF+VNRKv7G5hnNkXW16ZFwbQtKAdX3bGNz5CxqbksL1ioZoRQp - XrPUNkzzKKS5JBfLS5OtNGEb2ERLby9rs6nZ64mc7e1N72XjLZmOEvh2Rng4IwqsCjJe - FyXrGvFdDLyedJbh9Xg9SFYLw3Q0qvR1jYpC2T9GuHyEbnxzDFJbriBc8U9COPRdEB77 - nRCuHKH0JoSrkOZKhvC4fx3C429CuPofIxweoRuJvAWpDSsI1/yTEJ7wXRCu/U4I141Q - ehPC9UhzHUN44r8O4Uk3ITz5HyM8ZYRuJHIqUjtFQXjaPwnh6d8F4YbvhPCtI5TehHAj - 0nwrQ3jGvw7hmTch3PSPEZ41QjcSeRtSO0tBePY/CeE53wXh5u+EcMsIpTchPBdpbmEI - 3/6vQ3jeDQjjgrcG96QXce/F4Y6tOgpNeVGQi9D4oZP1uFm9iI6lMc59EAUeHWBc+gBO - 4RsAs/NOYSsChsUlpQaPIYCuht8SvfZfwplvJkT56VePYS2K61rgh7AfthtsCGdLGTyv - 4TJwh6mSM9QaWUu1WgriUlqpcug42Qf2FF2UaI55tm+y5eU1XJkeq2rQfz39ymWDMVQE - 1dVVsarqqiGMx0qK0zxmj2HYkX6+6No2Lu/aW9yDV89Rt3BmMF7TF9f1Y9f4IwodfZhQ - QShsY1SohqkQ7yQOjdKzWhMlc7DnD27s+TLr9Nsdevu5q9deoa/Fii4oHfXHFrE+dgKI - VuwjDX4dbqklUzkqEhVnIXbuEhHSiJMzadK1c0gz9yZ5n3tT875Wzav5lDr6fcrPoDsp - DapzUirUFSkT6RzaQyXfohQ15YwcoRqtkRNls9Xq4HkhSvaEU9RuTiPGtITGUtxGzDme - BnZTT5ctr0F/pWp67LL9SiiEf9tlhl9dR+1nUG1F5IzW0NSZq4+maKOkb5ASyljuG6CU - 2yhML7w/xq85v1FIhiXF0LpqJVnVujLNoyIeg9cwuryMeInZZDEbvDuJi+wnzxLHGT7e - +kJ8rvC8cOaqn3/vmwncwoKL914N8pcKyj8cfe3fFR3oS7wrFCEuZrBAVdhrFQJChZ5T - AxXG6lUWzmIxqXxah434THar7WnPdmSDiX6ISZ5BjyIYqq5qLSkmBpPVUjpqTHmZodSg - l6gnk/PbiYd0V7W8Ebu95FeTvx/fHN+8YTKdIJy51v30sqePzP8xt/nahfj/bI1/TdRb - SSoXQjmNxpOHcqRHhB+Ea58gTxMaJrMItRByn/AZoUv4TuFRnrPnUJ+R43jwGUVRIALl - RA5J5mWZyYFyewUge0W7tGW+Lc+OsNumx0Ih/NsbGN42qK5CyI0hsnF6Yd7GQlseAh9G - gRHgeNyTUlHYKK/Rn1c85KwVWleuXKWipYgx0SO4+34X+/yN2BeIq4v/5BtkiOkxBzMT - Hyi7z1Q8V6iCD8MVucVErUe9cgZKJ+mXqpbppZBs1Kq49FFStsql17oq82hhsPJEJa0c - lesz6iVBdgayrM4o6UVRuNxSwFWooa4yTZVUVeU0ScHcg9mO8elB55TUQIV93PhfkJ24 - 6T5JdsCwVK4ocrkcO39dMkPVQyglA+pWK47SwqHCIYKhwRoqKZ6wOpxTPsacBcTuI+Wp - HrBlpHvAkmnyEE8WjKEecLisHmL2oAd5eXlEX4V+3kMPPQStpDVbkfU4oiOpRJREMylH - yY/2e7MkUfKOJ6WjcPtqMGEl7EJHvFkBf4AF/rLR5WPSiG5Vwx0tOzydo1YsKGkig+PN - 2kfuf7zSoz4o/PnZMz33WH3aDENuvr8116Ia85sHt585tbP31bn5k/c/aXaKuhRn0RKy - XM63Fcxrmpbb9OKeSZN2xXY6szhug1as8YYnLfv5o9t/kkYuszkOTye4i3wDOCAdDoSL - DtjJLttBuc/GTZENe0wcZxJdDinFhaNfSk+36gNGwgWoweFSB6x2pytKpGOeVWv+pvNV - 04dCoRG9ZxH9kALlaLDLPq1Z7Qddmt5PjIZUvWTHlACchxDKcxpLih9SjeipbKKf8ET0 - EIYnwsqATfp5CrZgsXoLESyENYlgKYOOlumhVKJvf2Lt169a+7MpxY9u7XrE3p/x36df - /4YY33TyDZFLCx85uOLpfR9suvetF0jpZ3i0MlZADCoS73FDwjmc511wb3jUGN1E3Rzd - Af5QuuCTTTTVpQfZ5ZLS1NRl1QiFaYX6oMHocGsCDnuGe6NnVc2N7Mcu46w7xAa9IWRI - apHD5lSpgRCbBnlzogd26gd1uuxHBvGvaIyRqYKiIKIZrBYrThLeMsYWlI02ln69dd+a - ffvvf/QQ6W0qHnfkmeqf3XUs/s2XvyV3fH7p5V//x8Vf0TGjM6ZS1zfjty9sJgXf/IHM - wfE2KfEe78DTHieeDPqINrx6p/yU44CbE3Q0VTCZdcZUsymsDZvkoINM1RznLpAXuQvp - 78jvqt52v+P93Pq5V3PBcMFI58mCJzt1t8WVHRIlyeJxOSW1y6LxSTudB5wnnJecvM+S - 6nMKdrVWMugCqa6A4AhkF0oBu90feNOzvzUJUOyyMim+GQsZQzjkQhgUtSrzI9MTtI76 - IcxVtKUevLzA4VEaEXjR7Tfojfo0vUnPi1pfVnq2HzLB5ScZLpVV8oPGrPOTFJ3X4cEs - AT3ZhnqVokePDcvkuFTGZm5e7kNkZSusbG1FFcLH7MnAkTimfAwqEI5LEdE2oBIRfwAH - qigROvh2RblRf+1L4Ymdj88qNh2Vbi2ZufqWmS/F/0Bs/0XcmpwpRx48KBAvP/HO22Ys - n/LMsy+0lk+sfLKw0anHuVDEGbMm7r+n/uFjveSD5Bw4Ll7JfY4ycUMBnvSeCE8vN02W - J6ua5RbVo9pD6QddhwL7806ma8IyZ8kK6s6rs3Ca48Wgy642utSphVJhoeDkCi2FBUHB - UazVBVLG+wNOe1HxDYp4ZSjEkI5d/grxTFogppEKvEl88705jgyNIdun93sz/H7IcaBn - 0Og8kKrTpvhcWX4SSA/ieNQaPQqKw6MQ4VS0lWloWanBJImeLH+gFKFkMCozWDZDEJSJ - ThmdOO0R+uD80rL9VV3xl4/8UXciJTDukVfDfq5815rn4leJdIrU/uTfnq/3bXvw3K35 - 8df4mvHeCRuvjXql5709P50UqNo6+8OZjX9Go51CCuP7zg7csfvnZ/oXrqcFCCjB02pQ - xq4FmsL5qJ2yVbLKAT6Qdo90jyynpdA0PLE3uETJrFWnBNVoqc1BsKCtjhLxmGdBcuzi - qkNZqg0xw8dGbogwRYTWtFIDztvJyRpXEYpa4BJi/WC4dM7DXzQVnMwo2dh1fFA4F/tg - hif0bMve2Az6bM+Y5t1vx15i8qaMPlKJBpCtVcvDTulTHokWObUKDTHqR1DicGJU9f2N - kvOxqvOKFWaLt+rpOHsiEV5Dqdm7/gT++Nyrbwtn2I0NgU3ojVPaDoaRS04tYKPYJnB2 - XrihSWQuuYyqTja2aXCQLXSv4yf6+Inghw3hSkmWdGKqVbbqrKkBOYBDeZJ9tmaJRuv1 - qR0ur11NeavP47K6UkQJxHSnj0tT52CfhqApSsiAI4gGgYRxriv0ofLYAzlRknIjyJf1 - V4auxIaJwTVdNZoLHPPWEDO61xE3DyNuvW4lEXg2HBH3GyQwEB7dsnJdQ3521TMd7zTk - nr5z+rKnTjiCXYsPDPJFu27NHledXT+76UeztsTG0M/vbNyyP/YkPb1i1NS9rzLJKHLh - hnAc2tHyzQ+XnBAviJQXTWLA1CN2S4JJS002vUtANm0atUNyOEAbVDmcpNAWtIM9HZcg - N6lPcmpLjjbka+hvKkRQicw3sMJ0COcaHUF+yPrD0/o6Lzfmn3AVrw0Hp1QUpA+SA0j/ - /Jk/nvMM06UFVYtSLDVlK5fGXkViUYsqE+/yHrTXWrx/scMT4dJd8g79U5af8gfl/fpD - lqj8knyJ/1T3hUk7VhZdNknrMmrskt1upoFUR7oqYLY70qNEhVZ7eFa+eaWaNNb5YOX9 - mjQVzqAG6ieSFWNCCsbUJq0fiB492YJGmtOhp8yxzGPGOdtYNjxK0DIbcTalHrRgimH+ - aEPxtFM/3bHjWbzkuhb/84fxa8T4e7GbpO7fMf+H1wYOX+bei/8xfiUeiz9H8q7hwinM - bHNP/Dbeh6zrIAu6w/mH5ANWmiNnOg060WWWUkWdy6nJ0tGAzZGtLtQXeoJZqXZv9kbP - mSR7bD+RlI1iaJhghk2M05IOgsPP+yEdGRMs6BG7zg+cVeFJYYst5bLRKidlZmYLeFKa - 1E+8eGD2ApdtBi998YCv/tTpOh/68cL+8vDtDxyPn+jevXpmceXg6jdeXzfv6OlFux+c - s587umVyTlX8C+TxmR13lGVMjn04PI7pVhyDBrg17A9w/pQx3ESe18l6qlMZVNqAzNTQ - oJYdaYStPcBuTIuSOhxYaxXDynhs0OMuqXp69fnYeWZZ2XhKzl+K6lmsZrZeYkNo02Hz - T+4UbC59uv7RrThUTpbvodzzHO1fFdvFxkVN4hJ3nJ+KtqmIFIZ/UKHaJewwPmXaZd6V - K+Zk+wLlnnrPxOyJgdnZcwKLs5f4V2tXp6zW9Xi7s7t93f79GQfz0zg0yUIBX5gGDnO6 - 1WkzF5gKc1I1S2W/r9xHfVkpaj4vzfai05Um8a7C3XmaIkml01MJijxFDrfNYgtYx+f4 - pUCOo0TnDujHQ6DQXlwyMLKOwCkkad9CeowxdkNF6OOQYzJmK3o2paxUFhLTSAH1m30O - v0fn9oDKL3kIl497AiEXYy4j5qWbbB6SmZrlAU+WLkUOqD3E71OpSQHvATGIXobB6SF2 - C3rKckJZiCqeoiLXFR+X/GmKGVTUpYgtIXApzyyH5E0uJ5j6uAlbdZhQcfwB8qXsqz24 - aNe4wN0/2HRL9/sn/3TnBNon+Mc/tXhpXU7Dvedqlr772y8vSOQEaZxbPGfO7XXZuALL - yp380K5fbJnbOW7UxIZwfa49zVWUX/fDH1x892n6V7QJ1sSXVCXMxdlh5s9TCtVndSRK - qsM+3hKycqJObXDgdI03nUEw68ypnJuj3DWL3e645lkyvIqPtYbOK4ux5DRdxCbpWNWQ - PnZZMR5oh5Ib2eF9i78M16mlB48fPuw3l6RkmNwTAmvnPvmkMDf+1rZYXUWahtAtKvmh - JfQFvNlH/VqX+IT7LY5nK1I4Pzw2anrJRFVpssmeZjfliPdyl9CEg6BTg5iiFnDuskk2 - G24NCtVBrcbhIEFG7OvXraWyzWbqj+JPrnOqq5hCMNUnrTftuL1jlPUdSsXgIxWO4kd+ - Uesb7KPe0Uu2fdpUwI5gYqGZo9sOzv13qrv62t5xubOemrmJvuNg41ODE+/HfBGGZey0 - Ce/m2fESh05kx0xF7DRJxKnSGDqFN/fXY/JwrLgkrTSdWFXEi3+S8cXXf30/vpOs/iz+ - dTx+mazmi+IbyWohdjX2Ptka/x71IUzYp/L7UX116x2pVV+BQVbSL140/I5FlFATrxR9 - uGsBPBcars9CMRgP4icR5C8d14Y0T46UKO+jZxWMUCPMhn7+E+gX+2AnfqfQh+nR/N0w - kweoxLAC3SR049CtJxcUtwnrrmdpdKxOD+2DTVi/hobQWNwN6zCOOOH5xH3QR8rJWvIB - 3c/lcD/k5wsi3iyvF/aLWXin/LXUKT0nb5F/q6pQrVKos+JdOAd34vqI4pcWemjFDzE+ - V2sRScYVwS8TktyJWAbNLbfNamzKm9SxvKeje+nCdqxB0eEv0YHfBvy9nxUzc/Arg2L8 - OiIEtVCvfG0wRfmi4Fbli4eZ+BXDbTAb5kAz3J48T5yMZ4rV6MrQ5eXdYoN1ZD88ge5p - dBwsJY/BanSb0D2Fjh+JHcLUSfLYAC+HT5HV4CBTwhrePctkd9vUGvfruGwY3Ot+1/bJ - aWLHr0s+JvaBFFDdosaDnB/DInCTn+JO7X78GiKH7D4WXO5uw6JD0IVuHTpO8Qk5NJAx - yv08yQcfHse4iR8yeHLc/fuSAvenJVFKBtznAlEeg19mYCqc6j7r2uv+P64l7ufRHU4W - 9QWxxnH3Iddy97aMKNk94N7KFm8D7ieTwT0ufPW4e0Vwh3tRiVI+bUeUHh5wh7B8dljj - Lq/wuMtcl91FgahMMF3gmubOLflPdza+iNUysVFf2OB2ura5x2JRhqsuMBbdadJH9kAu - 2TPgm+I+hVFk99jkYMWOKHng2KScEl+U3B8un5SzIzgp4AtOc/uC9YEAxme/JK2Xbpdu - kUZJefhBAk7kUrpkko2yXtbJWlkty7IUJT8bqHaLp8lhqEZYDh+TRRmPHJ/DTP40OaJk - Hjkh8zKVQTZFEx8NMv3CpevhQVQtAhg5LioxMUqO4BkwyzoSdqNqE+CVAj1qW/ITI1RK - SmQKU/Dm9/GoCBssPdW2auN4Q6i+9v/ltSkl133FdPx9z0ZckR149xjpc7XgNS9GEq6W - 61XR6P9/ft33YIWOmjx2bnesp2vZYuXa2lvX0Ya315HHevAzgnULMjOPLusavpP3ty1Y - 2MnuTds7Il3ejtrIMm9t5tEe5T2WfUPxYlbc4609CovrZjUfXRzuqB3oCffUsev7Ywtq - VrXe1Nemkb5W1fydvmpYY6tYXwuU977VVysrXsD6amV9tbK+FoQXKH0xCOqWNtXc3Y3a - iVfbeLWc0xSZPGNuM37B0VIbJfvZffc98H8BcaX7nAplbmRzdHJlYW0KZW5kb2JqCjU2 - IDAgb2JqCjY2ODkKZW5kb2JqCjU3IDAgb2JqCjw8IC9UeXBlIC9Gb250RGVzY3JpcHRv - ciAvQXNjZW50IDc3MCAvQ2FwSGVpZ2h0IDcyNyAvRGVzY2VudCAtMjMwIC9GbGFncyAz - MgovRm9udEJCb3ggWy05NTEgLTQ4MSAxNDQ1IDExMjJdIC9Gb250TmFtZSAvWFlVVFBT - K0hlbHZldGljYSAvSXRhbGljQW5nbGUgMAovU3RlbVYgOTggL01heFdpZHRoIDE1MDAg - L1N0ZW1IIDg1IC9YSGVpZ2h0IDUzMSAvRm9udEZpbGUyIDU1IDAgUiA+PgplbmRvYmoK - NTggMCBvYmoKWyA2NjcgNjExIDAgMCAwIDAgMCAwIDgzMyAwIDAgMCAwIDAgMCAwIDcy - MiA2NjcgMCAwIDAgMCAwIDAgMCAwIDAgMCA1NTYgMAo1MDAgNTU2IDU1NiAwIDU1NiA1 - NTYgMjIyIDAgMCAyMjIgODMzIDU1NiA1NTYgNTU2IDAgMzMzIDUwMCAyNzggNTU2IDAg - MCA1MDAKXQplbmRvYmoKMjAgMCBvYmoKPDwgL1R5cGUgL0ZvbnQgL1N1YnR5cGUgL1Ry - dWVUeXBlIC9CYXNlRm9udCAvWFlVVFBTK0hlbHZldGljYSAvRm9udERlc2NyaXB0b3IK - NTcgMCBSIC9XaWR0aHMgNTggMCBSIC9GaXJzdENoYXIgNjkgL0xhc3RDaGFyIDEyMCAv - RW5jb2RpbmcgL01hY1JvbWFuRW5jb2RpbmcKPj4KZW5kb2JqCjEgMCBvYmoKPDwgL1Rp - dGxlIChVbnRpdGxlZCkgL0F1dGhvciAoVGhvbWFzIFJpc2JlcmcpIC9DcmVhdG9yIChP - bW5pR3JhZmZsZSkgL1Byb2R1Y2VyCihNYWMgT1MgWCAxMC41LjggUXVhcnR6IFBERkNv - bnRleHQpIC9DcmVhdGlvbkRhdGUgKEQ6MjAwOTA5MTExNDQwMzFaMDAnMDAnKQovTW9k - RGF0ZSAoRDoyMDA5MDkxMTE0NDAzMVowMCcwMCcpID4+CmVuZG9iagp4cmVmCjAgNTkK - MDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDQ5MjUyIDAwMDAwIG4gCjAwMDAwMDEwNDEg - MDAwMDAgbiAKMDAwMDAzNjY3MiAwMDAwMCBuIAowMDAwMDAwMDIyIDAwMDAwIG4gCjAw - MDAwMDEwMjIgMDAwMDAgbiAKMDAwMDAwMTE0NSAwMDAwMCBuIAowMDAwMDIxNzk2IDAw - MDAwIG4gCjAwMDAwMDUyMDQgMDAwMDAgbiAKMDAwMDAwNjEzMiAwMDAwMCBuIAowMDAw - MDAxMzY3IDAwMDAwIG4gCjAwMDAwMDIyODQgMDAwMDAgbiAKMDAwMDAwMjMwNCAwMDAw - MCBuIAowMDAwMDAzMTYxIDAwMDAwIG4gCjAwMDAwMDMxODEgMDAwMDAgbiAKMDAwMDAw - NDIyMCAwMDAwMCBuIAowMDAwMDA0MjQwIDAwMDAwIG4gCjAwMDAwMDUxODQgMDAwMDAg - biAKMDAwMDAzMTA0NSAwMDAwMCBuIAowMDAwMDQxNjkwIDAwMDAwIG4gCjAwMDAwNDkw - NzcgMDAwMDAgbiAKMDAwMDAzMDE4MCAwMDAwMCBuIAowMDAwMDA2MTUxIDAwMDAwIG4g - CjAwMDAwMDkwNDQgMDAwMDAgbiAKMDAwMDAyNzM4NSAwMDAwMCBuIAowMDAwMDE1MjI0 - IDAwMDAwIG4gCjAwMDAwMTc5NTYgMDAwMDAgbiAKMDAwMDAyNDU5MCAwMDAwMCBuIAow - MDAwMDA5MDY1IDAwMDAwIG4gCjAwMDAwMTIyNDYgMDAwMDAgbiAKMDAwMDAzNjYzNSAw - MDAwMCBuIAowMDAwMDEyMjY3IDAwMDAwIG4gCjAwMDAwMTUyMDMgMDAwMDAgbiAKMDAw - MDAzMzg0MCAwMDAwMCBuIAowMDAwMDE3OTc3IDAwMDAwIG4gCjAwMDAwMjA4NjAgMDAw - MDAgbiAKMDAwMDAyMDg4MSAwMDAwMCBuIAowMDAwMDIxNzc2IDAwMDAwIG4gCjAwMDAw - MjE4MzIgMDAwMDAgbiAKMDAwMDAyNDU2OSAwMDAwMCBuIAowMDAwMDI0NjI3IDAwMDAw - IG4gCjAwMDAwMjczNjQgMDAwMDAgbiAKMDAwMDAyNzQyMiAwMDAwMCBuIAowMDAwMDMw - MTU5IDAwMDAwIG4gCjAwMDAwMzAyMTcgMDAwMDAgbiAKMDAwMDAzMTAyNSAwMDAwMCBu - IAowMDAwMDMxMDgyIDAwMDAwIG4gCjAwMDAwMzM4MTkgMDAwMDAgbiAKMDAwMDAzMzg3 - NyAwMDAwMCBuIAowMDAwMDM2NjE0IDAwMDAwIG4gCjAwMDAwMzY3NTUgMDAwMDAgbiAK - MDAwMDAzNjgxOSAwMDAwMCBuIAowMDAwMDQxMjgwIDAwMDAwIG4gCjAwMDAwNDEzMDEg - MDAwMDAgbiAKMDAwMDA0MTUzNiAwMDAwMCBuIAowMDAwMDQxODczIDAwMDAwIG4gCjAw - MDAwNDg2NTIgMDAwMDAgbiAKMDAwMDA0ODY3MyAwMDAwMCBuIAowMDAwMDQ4OTA5IDAw - MDAwIG4gCnRyYWlsZXIKPDwgL1NpemUgNTkgL1Jvb3QgNTAgMCBSIC9JbmZvIDEgMCBS - IC9JRCBbIDxmOTA3NjFiZGExNmY3ZTJlMDkyMjA2Mjg3ZmQ4ZjAzYT4KPGY5MDc2MWJk - YTE2ZjdlMmUwOTIyMDYyODdmZDhmMDNhPiBdID4+CnN0YXJ0eHJlZgo0OTQ2MAolJUVP - RgoxIDAgb2JqCjw8L0F1dGhvciAoVGhvbWFzIFJpc2JlcmcpL0NyZWF0aW9uRGF0ZSAo - RDoyMDA5MDkxMTE0MTUwMFopL0NyZWF0b3IgKE9tbmlHcmFmZmxlIDUuMS4xKS9Nb2RE - YXRlIChEOjIwMDkwOTExMTQzODAwWikvUHJvZHVjZXIgKE1hYyBPUyBYIDEwLjUuOCBR - dWFydHogUERGQ29udGV4dCkvVGl0bGUgKFVudGl0bGVkKT4+CmVuZG9iagp4cmVmCjEg - MQowMDAwMDUwNzk4IDAwMDAwIG4gCnRyYWlsZXIKPDwvSUQgWzxmOTA3NjFiZGExNmY3 - ZTJlMDkyMjA2Mjg3ZmQ4ZjAzYT4gPGY5MDc2MWJkYTE2ZjdlMmUwOTIyMDYyODdmZDhm - MDNhPl0gL0luZm8gMSAwIFIgL1ByZXYgNDk0NjAgL1Jvb3QgNTAgMCBSIC9TaXplIDU5 - Pj4Kc3RhcnR4cmVmCjUwOTkzCiUlRU9GCg== - - QuickLookThumbnail - - TU0AKgAACkCAP+BACCQWDQeEQmFQuGQ2HQ+IRGJROKRWLReMRmNRSBP+Nx+QSGJtaSR2 - RSeUSAAysVS2Uy8ASaYTOaRNqzcVzmGvx5vN+AkAPd+A0IgiDPx4PB7hAIAl3ut7hEKg - 2Gvh6PgCAwEASEO9zOJ5gQJB4LAyKPx8VgEUaLTdqzkVzWRTK5XW7Qa3XCHNhWIpkBck - CN3tpxvoGDASghhKlhiAkiltOkPCwAOgEhYAOV7BoQABzOwGBkOgp9NFyAAXhZ5t15P1 - vtd7ksskB2M5pP4IgJ8AoLgVstUCCMQPF0v0GvJsPAHiABvl+iMUgpltcAFInDi2Qu8z - q7xq6d3wTPt3GFvNwr5fst9A4PhEEgMAPp4PF5v0DAh7uBtu4RC0MGQWhpAmFQRA2C4H - HqdZ5AIAgBgSCoKAcALiHYeR8AEAB+gIDoeBqCxXk2W4KhIDgUBiDJlFmaYLAsBJ/AwE - QRAafZznaegBgMCIFAUAADACAoTh6HAIoa8bwow78jyUkMjJq8xwnwD4Jn2bh5AcEgNL - Mgp+HUYZbGcD4jCQzCsAIrauIUfBummcoNhaEbsgAtC1TiiMmyWickzxPaLpIa09Jef1 - BHpQgHUMiJ80SftFx3HjwpWAKWhVPiJUBSlL0wgiOlFTheU8TVQATUSHGdUpC1OSFUg7 - VdM1ag9LVdWK7rSfByVsA1cAbXQI14iNBH8dtgndYddAaDNj0hWU+VhZVmpQdloWCdtj - gzQwHJBX50W0etuA5b1RKBZzwWZcVyoofd0VsclIW8DkGTQmCenmc16AlewK3xcy5XJf - V+oUeOAW0dAKYICeDSOfmEnLhdfg3h1cANfy5oHiWKoRX96HNWlVg6A+PUzYZ3HXkcWA - tXkiYtJGKZTlJw5djwDgvmVk2UfWbXVmQLgXneWI5ldyz9fmeotSFJIXoOf6GmmipboV - MTugp3mcWRqAmF5xkyXgajEH4KA4Cd5nofR/HuaBamYGwzisBByHAfMMHgdZzHqCYTAg - e54AoC4CHYfgEGoVZaAYHogBuDgJR6fp0nQf4LgUex7gUAx2nOfQIAAeTlAqB54nqBAS - g4dxtnIDgWhRLSEagg7xnUZxkHkCgFnCcgCA+Ch2msdoJBoDoBGwb4CBiD4APqeh1Ho5 - YLn7vqzHQdaxgSZBgG4FQdheAB6gGBZ9mmZB4BMJIYgqfB3HUe4CgAfZ/gUcJhGAAoWB - uEQHAOAoFcVxnHchyXKctzDmnOOedA6J0jpktHjacpd1TxBwjJFaLgZI4xzD9ASPIZwB - QkhoByBEfAwBbjbAmZgAYDgMj4GWKwd4Ow0BMAkOcaY+ANAiAkAIfI+B+gCK2OMYQ1wU - BSCEOoTwhRvAjB6iYFI4xWCaGeAgE4NgbA1BGUUfo+R1DsHgAMBgEkdAEH+BQE4NASAR - XgQWBcZCcE6HwNgSIihnA4B4PoYI2gAgZA8CgzA8gFASAsPUYQsR1gzCGCEBgDwCgCHo - OkZoyBpj5ACCEFINAMAOAmCUBI4BPixHcEcH4BhpDnAqC0DYDQAj7H0AAegyRcjfA2Dg - E5YwXAhFyIEUYJQkAkAQB8E8SIlRMidFCKUVIrRYi0AqLkXowRigQ0lZ0ZS0DvHeT5OQ - 8x2jxAQBhw4CB9jgHAO8B4FwInwH6PgdI7AANeAkAgfA9m/AOK2OcZorhcjtBcFIFQBB - 5AGAWBMBABwAG6HSPABDiB3DtHQNIZ4+gXBOBsBcfAAAAjwG+OMBwIQSgOTQmYrZCYyk - ESalwdRuwADuHIPQfq9wDD3W0PsCgEh9DojwBByQDAHD+W0PgCE7B/AFAMAoBoECzD0H - MNwcg/AKAYAUP6dM/wCAhBABaew5qhgSAcPYchuwIFbAFTqcQ7KA0hoJQahFCqGUOohR - KilFiCUYK5Mkjy+qNtKIxW+t9cCRVsX60itpMGRjrFvX0Klf1GkXGvYNTgog22HZyAOx - RF2mKTIVXiuhNbGwJsiQ5dA+xdWZHPZsMdnWaEVI6W5k68lir2AkuCytdLKWpIQrRlw4 - QMWxAfbMmiv5nDvZCAK3VpimAQs/axctq7gD2uIOO4wGrkLFTwtwerIbmW9tMxC4Cyrh - WRHldezY52OWBVcwkflt2QswtMta6al7qtDtvXsD962YMVuuPJkKiR8sntMu+8p3bzsW - uYxkEF/QC3/sizYfTISkjwZ2Atal9r7kwI7ZC6eBVi2KPgSKxpBcHF2I6vJd93LgYVYm - P9IyahfDRASDUDA7R1ASTeVS1o6hzJWA0BYtg9BxDTHOAIBgDAIAdKKmZeA5hkC+GkPs - CoNwcgtAilwbg6x+gOAqBnGSaRzDDGMN0DIMQcxhTQWgfmPktj4YS20c6uAIYxAbl4hS - d07j4HeOoAADQHXwABVJBhRi0gAx9GMhA9HujoBSDgEiaM2DfGoN0eAEAPOgnYWvQQ08 - qD7yuDKMI/B6DjGgN8f4NAWgeTSM4XAwB6gbBUD3TeX8u0ZIInMrg9x1j0zeU3OoAMwZ - oLxGc8hIbQ62AAO8ZIcRBDJCCDUCgCAJgAG8PkEAFKIkEMsDEFg/cDAhAiN4Yo4HtATB - AB4Ao6hkjHGuB8GAJx7juH2A4dw3h8A6CwDgfYxBdbWAiBcEAHAKAQAuP8bYrhpgQBQP - EWw3wKg8kEA0GANgECoFEOcJAPgGDvHkOsYI0B7hPCQC4c44BsjRGsAEDwBh2D/BICoC - 4Ax6D1AcCoJwPQSRmLedwACdx3jYGmPveY6Rqjf5wOQBYDgDD1HkOiCYIQKDhFyO8EgO - wUgbAAM0a4AgHjaGWAAHwOx4ivF6CkNYSBzi9G0AMDuMj7gUAYchgYJgDDyA8DsE4tRA - ChBSHAJA5BfDyB0EMEg+x2wSHlzEdIBADjiHUCsKwSgPjgFeLYeEkQFGcACN0Yg4h+gK - AWA8GIJQKD7ANPxuI5h1q1G4OAFAUQyhOyRywvWH0jHmGSNAbo+gFgaAoPocA1R7AMBF - mXbYzh3AbBuCkC4Bx4jkGcNEc4/lvApAcPvhyGACDzHiMwZI2QPAxBsD4IoMoLC4GN8U - DgGASgKHuN0cw+gFAWA8pMawuh4AfBiAVygDAKgLKeP8DoEh7DlRuOwdA/QXAoAAGkHI - AcA4AWH4H6HoWC3KBQBqBOAgAGAKRYZQ5e10o5AmTkHUGmFgF4GySCB+9+H+W4AAAgAW - HkGyG6HWHUGyGyAWBSB+BeAyHIFoGUH2Bm9u72HYHOHGHkBABsBWHw6AH6AWA8BSBCAQ - GqGEFcGkAAB4CqB8BWMwG2FgFsGzB3B6HWWGHKHaiIBw3uG8GAGsnKH0HUAGA4BgeGG4 - 3QAGHkHYAOBSA4HgGuHYHyGgGQHeCiDCB+AEHgUSHgHAHhAeckAA52BMBaBIdQrmIs1y - 5a1uVmHEHEHcA0A8A0z0Lq1ULWIuzVAqUyK8HEzmA8KKPC0oHeH4AZE/EvEyI+waJKmU - wWJQw8AAwvFYTxFcJAvzFjFtFuJOICAADgEAAAMAAAABAGsAAAEBAAMAAAABACEAAAEC - AAMAAAADAAAK7gEDAAMAAAABAAUAAAEGAAMAAAABAAIAAAERAAQAAAABAAAACAESAAMA - AAABAAEAAAEVAAMAAAABAAMAAAEWAAMAAAABAZgAAAEXAAQAAAABAAAKOAEcAAMAAAAB - AAEAAAE9AAMAAAABAAIAAAFTAAMAAAADAAAK9IdzAAcAAA9kAAAK+gAAAAAACAAIAAgA - AQABAAEAAA9kQVBQTAQAAABtbnRyUkdCIFhZWiAH2QAIAAUACQAIABFhY3NwQVBQTAAA - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLEdUTULFSMSs5/UDDo/MsBg6 - 75uEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5kZXNjAAABLAAAAGByWFla - AAACFAAAABRnWFlaAAACKAAAABRiWFlaAAACPAAAABRyVFJDAAACUAAAAgxnVFJDAAAE - XAAAAgxiVFJDAAAGaAAAAgx3dHB0AAAIdAAAABRjcHJ0AAAInAAAAIZia3B0AAAIiAAA - ABR2Y2d0AAAJJAAABhJjaGFkAAAPOAAAACxkbW5kAAABjAAAAFJkbWRkAAAB4AAAADJt - bHVjAAAAAAAAAAEAAAAMZW5VUwAAAEQAAAAcAEgAdQBlAHkAUABSAE8AIABDAG8AbABv - AHIAIABMAEMARAAgACgARAA2ADUAIABHADIALgAyACAAQQAwAC4AMAAwACltbHVjAAAA - AAAAAAEAAAAMZW5VUwAAADYAAAAcAFgALQBSAGkAdABlACAASQBuAGMALgAgACgAdwB3 - AHcALgB4AHIAaQB0AGUALgBjAG8AbQApAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAABYA - AAAcAEMAbwBsAG8AcgAgAEwAQwBEACAAMAAAWFlaIAAAAAAAAG4ZAABCdAAACBZYWVog - AAAAAAAAWr4AAI0oAAAbLFhZWiAAAAAAAAAt/AAAMGIAAK/nY3VydgAAAAAAAAEAAAAA - AAABAAMABwALABEAGAAgACkANABBAE4AXQBuAIAAlACpAMAA2ADyAQ0BKgFJAWkBiwGv - AdQB+wIkAk8CewKpAtkDCgM9A3IDqQPiBBwEWQSXBNcFGQVdBaIF6gYzBn4GywcaB2sH - vggTCGoIwwkdCXoJ2Qo5CpwLAQtnC9AMOgynDRYNhg35Dm4O5Q9eD9kQVhDVEVYR2RJe - EuYTbxP7FIkVGRWqFj8W1RdtGAgYpBlDGeQahxssG9Qcfh0pHdcehx86H+4gpSFeIhki - 1yOWJFglHCXjJqsndihDKRIp5Cq3K44sZi1ALh0u/C/eMMExpzKQM3o0ZzVWNkg3PDgy - OSo6JTsiPCE9Iz4nPy5ANkFBQk9DX0RxRYVGnEe1SNFJ70sPTDJNV05/T6lQ1VIEUzVU - aFWeVtdYEVlOWo5b0F0UXltfpGDwYj5jj2TiZjdnj2jpakZrpW0Hbmtv0nE7cqd0FXWF - dvh4bnnme2B83X5df9+BY4LqhHOF/4eOiR+KsoxIjeGPfJEZkrmUXJYBl6mZU5sAnK+e - YaAVocyjhqVCpwGowqqFrEyuFa/gsa6zf7VStyi5ALrbvLi+mMB7wmDESMYyyCDKD8wB - zfbP7tHo0+XV5Nfm2erb8d374AjiF+Qo5j3oVOpt7InuqPDK8u71Ffc++Wr7mf3K//9j - dXJ2AAAAAAAAAQAAAAAAAAEAAwAHAAsAEQAYACAAKQA0AEEATgBdAG4AgACUAKkAwADY - APIBDQEqAUkBaQGLAa8B1AH7AiQCTwJ7AqkC2QMKAz0DcgOpA+IEHARZBJcE1wUZBV0F - ogXqBjMGfgbLBxoHawe+CBMIagjDCR0JegnZCjkKnAsBC2cL0Aw6DKcNFg2GDfkObg7l - D14P2RBWENURVhHZEl4S5hNvE/sUiRUZFaoWPxbVF20YCBikGUMZ5BqHGywb1Bx+HSkd - 1x6HHzof7iClIV4iGSLXI5YkWCUcJeMmqyd2KEMpEinkKrcrjixmLUAuHS78L94wwTGn - MpAzejRnNVY2SDc8ODI5KjolOyI8IT0jPic/LkA2QUFCT0NfRHFFhUacR7VI0UnvSw9M - Mk1XTn9PqVDVUgRTNVRoVZ5W11gRWU5ajlvQXRReW1+kYPBiPmOPZOJmN2ePaOlqRmul - bQdua2/ScTtyp3QVdYV2+HhueeZ7YHzdfl1/34FjguqEc4X/h46JH4qyjEiN4Y98kRmS - uZRclgGXqZlTmwCcr55hoBWhzKOGpUKnAajCqoWsTK4Vr+CxrrN/tVK3KLkAutu8uL6Y - wHvCYMRIxjLIIMoPzAHN9s/u0ejT5dXk1+bZ6tvx3fvgCOIX5CjmPehU6m3sie6o8Mry - 7vUV9z75avuZ/cr//2N1cnYAAAAAAAABAAAAAAAAAQADAAcACwARABgAIAApADQAQQBO - AF0AbgCAAJQAqQDAANgA8gENASoBSQFpAYsBrwHUAfsCJAJPAnsCqQLZAwoDPQNyA6kD - 4gQcBFkElwTXBRkFXQWiBeoGMwZ+BssHGgdrB74IEwhqCMMJHQl6CdkKOQqcCwELZwvQ - DDoMpw0WDYYN+Q5uDuUPXg/ZEFYQ1RFWEdkSXhLmE28T+xSJFRkVqhY/FtUXbRgIGKQZ - QxnkGocbLBvUHH4dKR3XHocfOh/uIKUhXiIZItcjliRYJRwl4yarJ3YoQykSKeQqtyuO - LGYtQC4dLvwv3jDBMacykDN6NGc1VjZINzw4MjkqOiU7IjwhPSM+Jz8uQDZBQUJPQ19E - cUWFRpxHtUjRSe9LD0wyTVdOf0+pUNVSBFM1VGhVnlbXWBFZTlqOW9BdFF5bX6Rg8GI+ - Y49k4mY3Z49o6WpGa6VtB25rb9JxO3KndBV1hXb4eG555ntgfN1+XX/fgWOC6oRzhf+H - jokfirKMSI3hj3yRGZK5lFyWAZepmVObAJyvnmGgFaHMo4alQqcBqMKqhaxMrhWv4LGu - s3+1UrcouQC627y4vpjAe8JgxEjGMsggyg/MAc32z+7R6NPl1eTX5tnq2/Hd++AI4hfk - KOY96FTqbeyJ7qjwyvLu9RX3Pvlq+5n9yv//WFlaIAAAAAAAAPbVAAEAAAAA0ytYWVog - AAAAAAAAAHoAAAB+AAAAaG1sdWMAAAAAAAAAAQAAAAxlblVTAAAAagAAABwAQwBvAHAA - eQByAGkAZwBoAHQAIAAoAGMAKQAgAFgALQBSAGkAdABlACwAIAAyADAAMAAxAC0AMgAw - ADAANwAuACAAQQBsAGwAIABSAGkAZwBoAHQAcwAgAFIAZQBzAGUAcgB2AGUAZAAuAAB2 - Y2d0AAAAAAAAAAAAAwEAAAIAAAFtAtkERgWyBx8Iiwn4C2QM0Q49D6oRFhKDE+8VXBZh - F2cYbBlyGncbfRyCHYgejR+TIJkhniKkI6kkryWWJn0nZShMKTMqGysCK+ks0S24Lp8v - hzBuMVUyPTMmNBA0+jXjNs03tzigOYo6cztdPEc9MD4aPwQ/7UDsQetC6UPoROdF5Ubk - R+JI4UngSt5L3UzcTdpO2U/qUPpSC1McVCxVPVZOV15Yb1mAWpBboVyyXcJe01/gYOxh - +WMFZBJlHmYrZzdoRGlQal1ramx2bYNuj2+lcLtx0XLmc/x1EnYodz54U3lpen97lXyr - fcB+1n/SgM+By4LHg8OEwIW8hriHtIiwia2KqYuljKGNno6Cj2aQSpEvkhOS95PclMCV - pJaJl22YUZk1mhqa/pvcnLqdl551n1OgMKEOoeyiyqOnpIWlY6ZBpx6n/Kjdqb2qnat+ - rF6tP64frv+v4LDAsaGygbNitEK1IrYCtuG3wLifuX66Xbs8vBu8+r3Zvri/l8B2wVbC - NcMHw9nErMV+xlDHI8f1yMfJmcpsyz7MEMzjzbXOh89E0ALQv9F80jnS9tOz1HDVLdXq - 1qfXZdgi2N/ZnNpP2wHbtNxn3Rrdzd6A3zLf5eCY4Uvh/uKw42PkFuS+5WbmDea1513o - BOis6VTp/Oqj60vr8+ya7ULt6gAAAS4CWwOJBLcF5AcSCEAJbQqbC8kM9g4kD1IQgBGt - EogTYxQ+FRkV9BbOF6kYhBlfGjobFRvwHMsdpR6AHxsftiBRIOwhhyIiIr0jWCPzJI4l - KSXEJl8m+ieVKEIo8CmdKksq+CumLFMtAS2uLlsvCS+2MGQxETG/MnIzJTPYNIs1PjXy - NqU3WDgLOL45cTokOtg7izw+PQQ9yT6PP1VAG0DhQadCbEMyQ/hEvkWERklHD0fVSLBJ - ikplSz9MGkz1Tc9Oqk+EUF9ROVIUUu9TyVSkVY1Wd1dgWElZM1ocWwZb71zZXcJerF+V - YH5haGJRYylkAWTZZbBmiGdgaDhpD2nnar9rl2xvbUZuHm72b8JwjnFZciVy8XO9dIl1 - VHYgdux3uHiEeU96G3rne8l8rH2OfnB/U4A1gReB+oLcg76EoYWDhmWHSIgqiN+JlYpK - iwCLtYxrjSCN1o6Lj0GP9pCskWGSF5LMk4uUSpUIlceWhpdEmAOYwpmAmj+a/pu8nHud - Op34nrOfbaAnoOGhnKJWoxCjyqSFpT+l+aazp26oKKjiqYqqM6rbq4OsK6zUrXyuJK7N - r3WwHbDFsW6yFrK+s2u0F7TEtXC2HbbJt3a4IrjPuXu6KLrUu4G8LbzavYu+Pb7vv6DA - UsEDwbXCZsMYw8nEe8Usxd7Gj8dBAAABPAJ4A7QE8AYsB2gIpAngCxwMWA2UDtAQDBFI - EoQTZxRJFSwWDhbxF9MYthmYGnsbXhxAHSMeBR7oH8ogeCEmIdMigSMvI9wkiiU4JeYm - kydBJ+8onClKKfgqqCtYLAgsuS1pLhkuyS95MCkw2jGKMjoy6jOaNEo1DjXRNpU3WDgc - ON85ozpmOyo77TyxPXQ+Nz77P75AmEFxQktDJEP+RNdFsUaKR2RIPUkXSfBKykujTH1N - c05pT2BQVlFNUkNTOlQwVSdWHVcTWApZAFn3Wu1b21zIXbZepF+RYH9hbWJaY0hkNmUj - ZhFm/2fsaNpp0mrLa8RsvG21bq5vpnCfcZhykHOJdIJ1enZzd2x4bHltem57b3xvfXB+ - cX9ygHKBc4J0g3SEdYV2hneHXYhDiSmKD4r1i9uMwY2njo2Pc5BZkT+SJZMLk/GU1ZW6 - lp6Xg5hnmUuaMJsUm/mc3Z3CnqafiqBvoVOiR6M6pC2lIKYUpwen+qjtqeGq1KvHrLut - rq6hr5SwmbGfsqSzqbSutbO2uLe9uMK5x7rNu9K8173cvuG//MEXwjPDTsRpxYTGn8e7 - yNbJ8csMzCfNQ85ez3nQm9G90t/UAdUj1kbXaNiK2azaztvw3RLeNN9W4HjikuSs5sbo - 4Or67RTvL/FJ82P1ffeX+bH7y/3l//8AAHNmMzIAAAAAAAEN+QAAB+QAAAIBAAAMYwAA - 9SH////2AAABX////RUAARx2 - - ReadOnly - NO - RowAlign - 1 - RowSpacing - 36 - SheetTitle - Canvas 1 - SmartAlignmentGuidesActive - YES - SmartDistanceGuidesActive - YES - UniqueID - 1 - UseEntirePage - - VPages - 1 - WindowInfo - - CurrentSheet - 0 - ExpandedCanvases - - - name - Canvas 1 - - - Frame - {{218, 52}, {999, 826}} - ListView - - OutlineWidth - 142 - RightSidebar - - ShowRuler - - Sidebar - - SidebarWidth - 120 - VisibleRegion - {{-54, -59}, {864, 672}} - Zoom - 1 - ZoomValues - - - Canvas 1 - 1 - 1 - - - - saveQuickLookFiles - YES - - diff --git a/framework-docs/src/docs/asciidoc/images/oxm-exceptions.png b/framework-docs/src/docs/asciidoc/images/oxm-exceptions.png deleted file mode 100644 index 8515e7c4887a..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/oxm-exceptions.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/prototype.png b/framework-docs/src/docs/asciidoc/images/prototype.png deleted file mode 100644 index 26fa2c1cf2d9..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/prototype.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/singleton.png b/framework-docs/src/docs/asciidoc/images/singleton.png deleted file mode 100644 index 591520ec1dcc..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/singleton.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/spring-mvc-and-webflux-venn.png b/framework-docs/src/docs/asciidoc/images/spring-mvc-and-webflux-venn.png deleted file mode 100644 index 6e0eeab744d4..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/spring-mvc-and-webflux-venn.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/tx.png b/framework-docs/src/docs/asciidoc/images/tx.png deleted file mode 100644 index 06f2e77c76f8..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/tx.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/tx_prop_required.png b/framework-docs/src/docs/asciidoc/images/tx_prop_required.png deleted file mode 100644 index 218790aca635..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/tx_prop_required.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/images/tx_prop_requires_new.png b/framework-docs/src/docs/asciidoc/images/tx_prop_requires_new.png deleted file mode 100644 index a8ece48193f3..000000000000 Binary files a/framework-docs/src/docs/asciidoc/images/tx_prop_requires_new.png and /dev/null differ diff --git a/framework-docs/src/docs/asciidoc/index-docinfo-header.html b/framework-docs/src/docs/asciidoc/index-docinfo-header.html deleted file mode 100644 index 485f2e4e9803..000000000000 --- a/framework-docs/src/docs/asciidoc/index-docinfo-header.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/framework-docs/src/docs/asciidoc/spring-framework.adocbook b/framework-docs/src/docs/asciidoc/spring-framework.adocbook deleted file mode 100644 index e020f7337c2d..000000000000 --- a/framework-docs/src/docs/asciidoc/spring-framework.adocbook +++ /dev/null @@ -1,26 +0,0 @@ -:noheader: -:toc: -:toclevels: 4 -:tabsize: 4 -include::attributes.adoc[] -= Spring Framework Documentation -Rod Johnson; Juergen Hoeller; Keith Donald; Colin Sampaleanu; Rob Harrop; Thomas Risberg; Alef Arendsen; Darren Davison; Dmitriy Kopylenko; Mark Pollack; Thierry Templier; Erwin Vervaet; Portia Tung; Ben Hale; Adrian Colyer; John Lewis; Costin Leau; Mark Fisher; Sam Brannen; Ramnivas Laddad; Arjen Poutsma; Chris Beams; Tareq Abedrabbo; Andy Clement; Dave Syer; Oliver Gierke; Rossen Stoyanchev; Phillip Webb; Rob Winch; Brian Clozel; Stephane Nicoll; Sebastien Deleuze; Jay Bryant; Mark Paluch - -NOTE: This documentation is also available in {docs-spring-framework}/reference/html/index.html[HTML] format. - -[[legal]] -== Legal - -Copyright © 2002 - 2023 VMware, Inc. All Rights Reserved. - -Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. - -include::overview.adoc[leveloffset=+1] -include::core.adoc[leveloffset=+1] -include::testing.adoc[leveloffset=+1] -include::data-access.adoc[leveloffset=+1] -include::web.adoc[leveloffset=+1] -include::web-reactive.adoc[leveloffset=+1] -include::integration.adoc[leveloffset=+1] -include::languages.adoc[leveloffset=+1] -include::appendix.adoc[leveloffset=+1] diff --git a/framework-docs/src/docs/dist/license.txt b/framework-docs/src/docs/dist/license.txt index 89cf3d232fa7..a5fb64294f36 100644 --- a/framework-docs/src/docs/dist/license.txt +++ b/framework-docs/src/docs/dist/license.txt @@ -263,10 +263,10 @@ JavaPoet 1.13.0 is licensed under the Apache License, version 2.0, the text of which is included above. ->>> Objenesis 3.2 (org.objenesis:objenesis:3.2): +>>> Objenesis 3.4 (org.objenesis:objenesis:3.4): Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.2 is licensed under the +http://objenesis.org/download.html, Objenesis 3.4 is licensed under the Apache License, version 2.0, the text of which is included above. Per the NOTICE file in the Objenesis ZIP distribution downloaded from diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/aopajltwspring/ApplicationConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/aopajltwspring/ApplicationConfiguration.java new file mode 100644 index 000000000000..4f1456b36e19 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/aopajltwspring/ApplicationConfiguration.java @@ -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.core.aop.aopajltwspring; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableLoadTimeWeaving; + +// tag::snippet[] +@Configuration +@EnableLoadTimeWeaving +public class ApplicationConfiguration { +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/aopajltwspring/CustomWeaverConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/aopajltwspring/CustomWeaverConfiguration.java new file mode 100644 index 000000000000..0a51c937e296 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/aopajltwspring/CustomWeaverConfiguration.java @@ -0,0 +1,35 @@ +/* + * 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.aop.aopajltwspring; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableLoadTimeWeaving; +import org.springframework.context.annotation.LoadTimeWeavingConfigurer; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver; + +// tag::snippet[] +@Configuration +@EnableLoadTimeWeaving +public class CustomWeaverConfiguration implements LoadTimeWeavingConfigurer { + + @Override + public LoadTimeWeaver getLoadTimeWeaver() { + return new ReflectiveLoadTimeWeaver(); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/aopatconfigurable/ApplicationConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/aopatconfigurable/ApplicationConfiguration.java new file mode 100644 index 000000000000..a7b6bb6496d2 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/aopatconfigurable/ApplicationConfiguration.java @@ -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.core.aop.aopatconfigurable; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.aspectj.EnableSpringConfigured; + +// tag::snippet[] +@Configuration +@EnableSpringConfigured +public class ApplicationConfiguration { +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopaspectjsupport/ApplicationConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopaspectjsupport/ApplicationConfiguration.java new file mode 100644 index 000000000000..a45001fdadab --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopaspectjsupport/ApplicationConfiguration.java @@ -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.core.aop.ataspectj.aopaspectjsupport; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +// tag::snippet[] +@Configuration +@EnableAspectJAutoProxy +public class ApplicationConfiguration { +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectj/ApplicationConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectj/ApplicationConfiguration.java new file mode 100644 index 000000000000..522acd7ea6d9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectj/ApplicationConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.core.aop.ataspectj.aopataspectj; + +import org.springframework.context.annotation.Bean; + +// tag::snippet[] +public class ApplicationConfiguration { + + @Bean + public NotVeryUsefulAspect myAspect() { + NotVeryUsefulAspect myAspect = new NotVeryUsefulAspect(); + // Configure properties of the aspect here + return myAspect; + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectj/NotVeryUsefulAspect.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectj/NotVeryUsefulAspect.java new file mode 100644 index 000000000000..337fe1265143 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectj/NotVeryUsefulAspect.java @@ -0,0 +1,24 @@ +/* + * 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.aop.ataspectj.aopataspectj; + +import org.aspectj.lang.annotation.Aspect; + +// tag::snippet[] +@Aspect +public class NotVeryUsefulAspect { +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ApplicationConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ApplicationConfiguration.java new file mode 100644 index 000000000000..abba61921144 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ApplicationConfiguration.java @@ -0,0 +1,37 @@ +/* + * 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.aop.ataspectj.aopataspectjexample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +// tag::snippet[] +@Configuration +@EnableAspectJAutoProxy +public class ApplicationConfiguration { + + @Bean + public ConcurrentOperationExecutor concurrentOperationExecutor() { + ConcurrentOperationExecutor executor = new ConcurrentOperationExecutor(); + executor.setMaxRetries(3); + executor.setOrder(100); + return executor; + + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ConcurrentOperationExecutor.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ConcurrentOperationExecutor.java new file mode 100644 index 000000000000..a760f744918f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ConcurrentOperationExecutor.java @@ -0,0 +1,63 @@ +/* + * 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.aop.ataspectj.aopataspectjexample; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import org.springframework.core.Ordered; +import org.springframework.dao.PessimisticLockingFailureException; + +// tag::snippet[] +@Aspect +public class ConcurrentOperationExecutor implements Ordered { + + private static final int DEFAULT_MAX_RETRIES = 2; + + private int maxRetries = DEFAULT_MAX_RETRIES; + private int order = 1; + + public void setMaxRetries(int maxRetries) { + this.maxRetries = maxRetries; + } + + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Around("com.xyz.CommonPointcuts.businessService()") + public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { + int numAttempts = 0; + PessimisticLockingFailureException lockFailureException; + do { + numAttempts++; + try { + return pjp.proceed(); + } + catch(PessimisticLockingFailureException ex) { + lockFailureException = ex; + } + } while(numAttempts <= this.maxRetries); + throw lockFailureException; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/Idempotent.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/Idempotent.java new file mode 100644 index 000000000000..394f21b43a6a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/Idempotent.java @@ -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.core.aop.ataspectj.aopataspectjexample.service; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +// tag::snippet[] +@Retention(RetentionPolicy.RUNTIME) +// marker annotation +public @interface Idempotent { +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/SampleService.java b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/SampleService.java new file mode 100644 index 000000000000..70636063a604 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/SampleService.java @@ -0,0 +1,36 @@ +/* + * 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.aop.ataspectj.aopataspectjexample.service; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; + +import org.springframework.stereotype.Service; + +@Service +public class SampleService { + + // tag::snippet[] + @Around("execution(* com.xyz..service.*.*(..)) && " + + "@annotation(com.xyz.service.Idempotent)") + public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { + // ... + return pjp.proceed(pjp.getArgs()); + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aopapi/aopapipointcutsregex/JdkRegexpConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aopapi/aopapipointcutsregex/JdkRegexpConfiguration.java new file mode 100644 index 000000000000..84eb3a7f5a8f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aopapi/aopapipointcutsregex/JdkRegexpConfiguration.java @@ -0,0 +1,34 @@ +/* + * 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.aopapi.aopapipointcutsregex; + +import org.springframework.aop.support.JdkRegexpMethodPointcut; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +// tag::snippet[] +@Configuration +public class JdkRegexpConfiguration { + + @Bean + public JdkRegexpMethodPointcut settersAndAbsquatulatePointcut() { + JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); + pointcut.setPatterns(".*set.*", ".*absquatulate"); + return pointcut; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aopapi/aopapipointcutsregex/RegexpConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aopapi/aopapipointcutsregex/RegexpConfiguration.java new file mode 100644 index 000000000000..a56be401eba5 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aopapi/aopapipointcutsregex/RegexpConfiguration.java @@ -0,0 +1,38 @@ +/* + * 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.aopapi.aopapipointcutsregex; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.support.RegexpMethodPointcutAdvisor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +// tag::snippet[] +@Configuration +public class RegexpConfiguration { + + @Bean + public RegexpMethodPointcutAdvisor settersAndAbsquatulateAdvisor(Advice beanNameOfAopAllianceInterceptor) { + RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor(); + advisor.setAdvice(beanNameOfAopAllianceInterceptor); + advisor.setPatterns(".*set.*", ".*absquatulate"); + return advisor; + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/reflective/MyConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/reflective/MyConfiguration.java new file mode 100644 index 000000000000..109101f41b79 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/reflective/MyConfiguration.java @@ -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.core.aot.hints.reflective; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ReflectiveScan; + +@Configuration +@ReflectiveScan("com.example.app") +public class MyConfiguration { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/registerreflection/MyConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/registerreflection/MyConfiguration.java new file mode 100644 index 000000000000..89e0267f6e7e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/registerreflection/MyConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.core.aot.hints.registerreflection; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.annotation.RegisterReflection; +import org.springframework.context.annotation.Configuration; + +// tag::snippet[] +@Configuration +@RegisterReflection(classes = AccountService.class, memberCategories = + { MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS }) +class MyConfiguration { +} +// end::snippet[] + +class AccountService {} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/registerreflection/OrderService.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/registerreflection/OrderService.java new file mode 100644 index 000000000000..afc66e8b7c80 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/registerreflection/OrderService.java @@ -0,0 +1,34 @@ +/* + * 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.aot.hints.registerreflection; + +import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; +import org.springframework.stereotype.Component; + +// tag::snippet[] +@Component +class OrderService { + + @RegisterReflectionForBinding(Order.class) + public void process(Order order) { + // ... + } + +} +// end::snippet[] + +record Order() {} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflection.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflection.java index 93abd52e85a2..2772146e71ea 100644 --- a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflection.java +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflection.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. @@ -32,7 +32,7 @@ public void performReflection() { Class springVersion = ClassUtils.forName("org.springframework.core.SpringVersion", null); Method getVersion = ClassUtils.getMethod(springVersion, "getVersion"); String version = (String) getVersion.invoke(null); - logger.info("Spring version:" + version); + logger.info("Spring version: " + version); } catch (Exception exc) { logger.error("reflection failed", exc); 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/core/aot/hints/testing/SpellCheckServiceTests.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java index 6151f9cd3782..cd4d87731b60 100644 --- a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.java +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SpellCheckServiceTests.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,7 @@ import static org.assertj.core.api.Assertions.assertThat; -public class SpellCheckServiceTests { +class SpellCheckServiceTests { // tag::hintspredicates[] @Test diff --git a/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.java b/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.java new file mode 100644 index 000000000000..3bb14d4e7220 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.java @@ -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.beans.dependencies.beansfactorylazyinit; + +public class AnotherBean { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ApplicationConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ApplicationConfiguration.java new file mode 100644 index 000000000000..c1f1d3cc0548 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ApplicationConfiguration.java @@ -0,0 +1,38 @@ +/* + * 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.beans.dependencies.beansfactorylazyinit; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +@Configuration +public class ApplicationConfiguration { + + // tag::snippet[] + @Bean + @Lazy + ExpensiveToCreateBean lazy() { + return new ExpensiveToCreateBean(); + } + + @Bean + AnotherBean notLazy() { + return new AnotherBean(); + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.java b/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.java new file mode 100644 index 000000000000..1d08cf43247a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.java @@ -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.beans.dependencies.beansfactorylazyinit; + +public class ExpensiveToCreateBean { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/LazyConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/LazyConfiguration.java new file mode 100644 index 000000000000..7f2685ba14e1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/LazyConfiguration.java @@ -0,0 +1,28 @@ +/* + * 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.beans.dependencies.beansfactorylazyinit; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +// tag::snippet[] +@Configuration +@Lazy +public class LazyConfiguration { + // No bean will be pre-instantiated... +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/CustomerPreferenceDao.java b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/CustomerPreferenceDao.java new file mode 100644 index 000000000000..cadf841666ce --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/CustomerPreferenceDao.java @@ -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; + +public class CustomerPreferenceDao { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/FieldValueTestBean.java b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/FieldValueTestBean.java new file mode 100644 index 000000000000..6c795eb47517 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/FieldValueTestBean.java @@ -0,0 +1,35 @@ +/* + * 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; + +import org.springframework.beans.factory.annotation.Value; + +// tag::snippet[] +public class FieldValueTestBean { + + @Value("#{ systemProperties['user.region'] }") + private String defaultLocale; + + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + public String getDefaultLocale() { + return this.defaultLocale; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/MovieFinder.java b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/MovieFinder.java new file mode 100644 index 000000000000..aee9cba8d7f5 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/MovieFinder.java @@ -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; + +public class MovieFinder { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/MovieRecommender.java b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/MovieRecommender.java new file mode 100644 index 000000000000..8ac7b7d9f3ac --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/MovieRecommender.java @@ -0,0 +1,36 @@ +/* + * 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; + +import org.springframework.beans.factory.annotation.Value; + +// tag::snippet[] +public class MovieRecommender { + + private String defaultLocale; + + private CustomerPreferenceDao customerPreferenceDao; + + public MovieRecommender(CustomerPreferenceDao customerPreferenceDao, + @Value("#{systemProperties['user.country']}") String defaultLocale) { + this.customerPreferenceDao = customerPreferenceDao; + this.defaultLocale = defaultLocale; + } + + // ... +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/NumberGuess.java b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/NumberGuess.java new file mode 100644 index 000000000000..94b519cc56b9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/NumberGuess.java @@ -0,0 +1,35 @@ +/* + * 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; + +import org.springframework.beans.factory.annotation.Value; + +// tag::snippet[] +public class NumberGuess { + + private double randomNumber; + + @Value("#{ T(java.lang.Math).random() * 100.0 }") + public void setRandomNumber(double randomNumber) { + this.randomNumber = randomNumber; + } + + public double getRandomNumber() { + return randomNumber; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/PropertyValueTestBean.java b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/PropertyValueTestBean.java new file mode 100644 index 000000000000..1bcfad4b1d69 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/PropertyValueTestBean.java @@ -0,0 +1,35 @@ +/* + * 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; + +import org.springframework.beans.factory.annotation.Value; + +// tag::snippet[] +public class PropertyValueTestBean { + + private String defaultLocale; + + @Value("#{ systemProperties['user.region'] }") + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + public String getDefaultLocale() { + return this.defaultLocale; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/ShapeGuess.java b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/ShapeGuess.java new file mode 100644 index 000000000000..0fb0da1322cd --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/ShapeGuess.java @@ -0,0 +1,35 @@ +/* + * 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; + +import org.springframework.beans.factory.annotation.Value; + +// tag::snippet[] +public class ShapeGuess { + + private double initialShapeSeed; + + @Value("#{ numberGuess.randomNumber }") + public void setInitialShapeSeed(double initialShapeSeed) { + this.initialShapeSeed = initialShapeSeed; + } + + public double getInitialShapeSeed() { + return initialShapeSeed; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/SimpleMovieLister.java b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/SimpleMovieLister.java new file mode 100644 index 000000000000..9ce82ee6b5b4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/expressions/expressionsbeandef/SimpleMovieLister.java @@ -0,0 +1,37 @@ +/* + * 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; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +// tag::snippet[] +public class SimpleMovieLister { + + private MovieFinder movieFinder; + private String defaultLocale; + + @Autowired + public void configure(MovieFinder movieFinder, + @Value("#{ systemProperties['user.region'] }") String defaultLocale) { + this.movieFinder = movieFinder; + this.defaultLocale = defaultLocale; + } + + // ... +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/validation/formatconfiguringformattingglobaldatetimeformat/ApplicationConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/validation/formatconfiguringformattingglobaldatetimeformat/ApplicationConfiguration.java new file mode 100644 index 000000000000..7f151848c9c7 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/validation/formatconfiguringformattingglobaldatetimeformat/ApplicationConfiguration.java @@ -0,0 +1,58 @@ +/* + * 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.docs.core.validation.formatconfiguringformattingglobaldatetimeformat; + +import java.time.format.DateTimeFormatter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.datetime.DateFormatter; +import org.springframework.format.datetime.DateFormatterRegistrar; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; + +// tag::snippet[] +@Configuration +public class ApplicationConfiguration { + + @Bean + public FormattingConversionService conversionService() { + + // Use the DefaultFormattingConversionService but do not register defaults + DefaultFormattingConversionService conversionService = + new DefaultFormattingConversionService(false); + + // Ensure @NumberFormat is still supported + conversionService.addFormatterForFieldAnnotation( + new NumberFormatAnnotationFormatterFactory()); + + // Register JSR-310 date conversion with a specific global format + DateTimeFormatterRegistrar dateTimeRegistrar = new DateTimeFormatterRegistrar(); + dateTimeRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")); + dateTimeRegistrar.registerFormatters(conversionService); + + // Register date conversion with a specific global format + DateFormatterRegistrar dateRegistrar = new DateFormatterRegistrar(); + dateRegistrar.setFormatter(new DateFormatter("yyyyMMdd")); + dateRegistrar.registerFormatters(conversionService); + + return conversionService; + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.java new file mode 100644 index 000000000000..c25e2ab073d3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.core.validation.validationbeanvalidationspringmethod; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +// tag::snippet[] +@Configuration +public class ApplicationConfiguration { + + @Bean + public static MethodValidationPostProcessor validationPostProcessor() { + return new MethodValidationPostProcessor(); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.java new file mode 100644 index 000000000000..ad18753eafe3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.java @@ -0,0 +1,34 @@ +/* + * 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.validation.validationbeanvalidationspringmethodexceptions; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +// tag::snippet[] +@Configuration +public class ApplicationConfiguration { + + @Bean + public static MethodValidationPostProcessor validationPostProcessor() { + MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); + processor.setAdaptConstraintViolations(true); + return processor; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.java new file mode 100644 index 000000000000..bdcfc4b23233 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.java @@ -0,0 +1,59 @@ +/* + * 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.jdbccomplextypes; + +import java.sql.Connection; +import java.sql.SQLException; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import oracle.jdbc.driver.OracleConnection; + +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.jdbc.core.support.AbstractSqlTypeValue; + +@SuppressWarnings("unused") +class SqlTypeValueFactory { + + void createStructSample() throws ParseException { + // tag::struct[] + TestItem testItem = new TestItem(123L, "A test item", + new SimpleDateFormat("yyyy-M-d").parse("2010-12-31")); + + SqlTypeValue value = new AbstractSqlTypeValue() { + protected Object createTypeValue(Connection connection, int sqlType, String typeName) throws SQLException { + Object[] item = new Object[] { testItem.getId(), testItem.getDescription(), + new java.sql.Date(testItem.getExpirationDate().getTime()) }; + return connection.createStruct(typeName, item); + } + }; + // end::struct[] + } + + void createOracleArray() { + // tag::oracle-array[] + Long[] ids = new Long[] {1L, 2L}; + + SqlTypeValue value = new AbstractSqlTypeValue() { + protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { + return conn.unwrap(OracleConnection.class).createOracleArray(typeName, ids); + } + }; + // end::oracle-array[] + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.java new file mode 100644 index 000000000000..b0968b877f8f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.java @@ -0,0 +1,61 @@ +/* + * 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.jdbccomplextypes; + +import java.util.Date; + +class TestItem { + + private Long id; + + private String description; + + private Date expirationDate; + + public TestItem() { + } + + public TestItem(Long id, String description, Date expirationDate) { + this.id = id; + this.description = description; + this.expirationDate = expirationDate; + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getExpirationDate() { + return this.expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.java new file mode 100644 index 000000000000..1343dcfbaee0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.java @@ -0,0 +1,46 @@ +/* + * 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.jdbccomplextypes; + +import java.sql.CallableStatement; +import java.sql.Struct; +import java.sql.Types; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.object.StoredProcedure; + +@SuppressWarnings("unused") +public class TestItemStoredProcedure extends StoredProcedure { + + public TestItemStoredProcedure(DataSource dataSource) { + super(dataSource, "get_item"); + declareParameter(new SqlOutParameter("item", Types.STRUCT, "ITEM_TYPE", + (CallableStatement cs, int colIndx, int sqlType, String typeName) -> { + Struct struct = (Struct) cs.getObject(colIndx); + Object[] attr = struct.getAttributes(); + TestItem item = new TestItem(); + item.setId(((Number) attr[0]).longValue()); + item.setDescription((String) attr[1]); + item.setExpirationDate((java.util.Date) attr[2]); + return item; + })); + // ... + } + +} \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java new file mode 100644 index 000000000000..2fd9109fc793 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java @@ -0,0 +1,39 @@ +/* + * 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.jdbcdatasource; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class BasicDataSourceConfiguration { + + // tag::snippet[] + @Bean(destroyMethod = "close") + BasicDataSource dataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java new file mode 100644 index 000000000000..45e842fdbf46 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java @@ -0,0 +1,40 @@ +/* + * 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.jdbcdatasource; + +import java.beans.PropertyVetoException; + +import com.mchange.v2.c3p0.ComboPooledDataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class ComboPooledDataSourceConfiguration { + + // tag::snippet[] + @Bean(destroyMethod = "close") + ComboPooledDataSource dataSource() throws PropertyVetoException { + ComboPooledDataSource dataSource = new ComboPooledDataSource(); + dataSource.setDriverClass("org.hsqldb.jdbcDriver"); + dataSource.setJdbcUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUser("sa"); + dataSource.setPassword(""); + return dataSource; + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java new file mode 100644 index 000000000000..0783c9228907 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java @@ -0,0 +1,38 @@ +/* + * 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.jdbcdatasource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +@Configuration +class DriverManagerDataSourceConfiguration { + + // tag::snippet[] + @Bean + DriverManagerDataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.java new file mode 100644 index 000000000000..a48a3c6e800b --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.java @@ -0,0 +1,40 @@ +/* + * 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.jdbcembeddeddatabase; + +import javax.sql.DataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +@Configuration +public class JdbcEmbeddedDatabaseConfiguration { + + // tag::snippet[] + @Bean + DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(EmbeddedDatabaseType.H2) + .addScripts("schema.sql", "test-data.sql") + .build(); + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventDao.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventDao.java new file mode 100644 index 000000000000..9762d8ae3804 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventDao.java @@ -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; + +public interface CorporateEventDao { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventRepository.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventRepository.java new file mode 100644 index 000000000000..a0eb1fe7f881 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventRepository.java @@ -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; + +public interface CorporateEventRepository { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDao.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDao.java new file mode 100644 index 000000000000..36ab9956b738 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDao.java @@ -0,0 +1,34 @@ +/* + * 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; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcTemplate; + +// tag::snippet[] +public class JdbcCorporateEventDao implements CorporateEventDao { + + private final JdbcTemplate jdbcTemplate; + + public JdbcCorporateEventDao(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + // JDBC-backed implementations of the methods on the CorporateEventDao follow... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDaoConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDaoConfiguration.java new file mode 100644 index 000000000000..799b81835584 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDaoConfiguration.java @@ -0,0 +1,30 @@ +package org.springframework.docs.dataaccess.jdbc.jdbcjdbctemplateidioms; + +import javax.sql.DataSource; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JdbcCorporateEventDaoConfiguration { + + // tag::snippet[] + @Bean + JdbcCorporateEventDao corporateEventDao(DataSource dataSource) { + return new JdbcCorporateEventDao(dataSource); + } + + @Bean(destroyMethod = "close") + BasicDataSource dataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepository.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepository.java new file mode 100644 index 000000000000..1597edd00af1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepository.java @@ -0,0 +1,37 @@ +/* + * 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; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +// tag::snippet[] +@Repository +public class JdbcCorporateEventRepository implements CorporateEventRepository { + + private JdbcTemplate jdbcTemplate; + + // Implicitly autowire the DataSource constructor parameter + public JdbcCorporateEventRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + // JDBC-backed implementations of the methods on the CorporateEventRepository follow... +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepositoryConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepositoryConfiguration.java new file mode 100644 index 000000000000..6a95d3f67e52 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepositoryConfiguration.java @@ -0,0 +1,25 @@ +package org.springframework.docs.dataaccess.jdbc.jdbcjdbctemplateidioms; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +// tag::snippet[] +@Configuration +@ComponentScan("org.example.jdbc") +public class JdbcCorporateEventRepositoryConfiguration { + + @Bean(destroyMethod = "close") + BasicDataSource dataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/cache/cacheannotationenable/CacheConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cacheannotationenable/CacheConfiguration.java new file mode 100644 index 000000000000..3c86a477e952 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cacheannotationenable/CacheConfiguration.java @@ -0,0 +1,37 @@ +/* + * 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.cache.cacheannotationenable; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +// tag::snippet[] +@Configuration +@EnableCaching +class CacheConfiguration { + + @Bean + CacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCacheSpecification("..."); + return cacheManager; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CacheConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CacheConfiguration.java new file mode 100644 index 000000000000..d2548ef6d771 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CacheConfiguration.java @@ -0,0 +1,33 @@ +/* + * 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.cache.cachestoreconfigurationcaffeine; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class CacheConfiguration { + + // tag::snippet[] + @Bean + CacheManager cacheManager() { + return new CaffeineCacheManager(); + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.java new file mode 100644 index 000000000000..a5c27321cfb3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.java @@ -0,0 +1,37 @@ +/* + * 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.cache.cachestoreconfigurationcaffeine; + +import java.util.List; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class CustomCacheConfiguration { + + // tag::snippet[] + @Bean + CacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCacheNames(List.of("default", "books")); + return cacheManager; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationjdk/CacheConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationjdk/CacheConfiguration.java new file mode 100644 index 000000000000..87aec966c0f0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationjdk/CacheConfiguration.java @@ -0,0 +1,54 @@ +/* + * 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.cache.cachestoreconfigurationjdk; + +import java.util.Set; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class CacheConfiguration { + + // tag::snippet[] + @Bean + ConcurrentMapCacheFactoryBean defaultCache() { + ConcurrentMapCacheFactoryBean cache = new ConcurrentMapCacheFactoryBean(); + cache.setName("default"); + return cache; + } + + @Bean + ConcurrentMapCacheFactoryBean booksCache() { + ConcurrentMapCacheFactoryBean cache = new ConcurrentMapCacheFactoryBean(); + cache.setName("books"); + return cache; + } + + @Bean + CacheManager cacheManager(ConcurrentMapCache defaultCache, ConcurrentMapCache booksCache) { + + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(Set.of(defaultCache, booksCache)); + return cacheManager; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationjsr107/CacheConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationjsr107/CacheConfiguration.java new file mode 100644 index 000000000000..874614ef7b90 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationjsr107/CacheConfiguration.java @@ -0,0 +1,41 @@ +/* + * 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.cache.cachestoreconfigurationjsr107; + +import javax.cache.Caching; +import javax.cache.spi.CachingProvider; + +import org.springframework.cache.jcache.JCacheCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class CacheConfiguration { + + // tag::snippet[] + @Bean + javax.cache.CacheManager jCacheManager() { + CachingProvider cachingProvider = Caching.getCachingProvider(); + return cachingProvider.getCacheManager(); + } + + @Bean + org.springframework.cache.CacheManager cacheManager(javax.cache.CacheManager jCacheManager) { + return new JCacheCacheManager(jCacheManager); + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationnoop/CacheConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationnoop/CacheConfiguration.java new file mode 100644 index 000000000000..afb57bada1d4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/cache/cachestoreconfigurationnoop/CacheConfiguration.java @@ -0,0 +1,46 @@ +/* + * 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.cache.cachestoreconfigurationnoop; + +import java.util.List; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.support.CompositeCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class CacheConfiguration { + + private CacheManager jdkCache() { + return null; + } + + private CacheManager gemfireCache() { + return null; + } + + // tag::snippet[] + @Bean + CacheManager cacheManager(CacheManager jdkCache, CacheManager gemfireCache) { + CompositeCacheManager cacheManager = new CompositeCacheManager(); + cacheManager.setCacheManagers(List.of(jdkCache, gemfireCache)); + cacheManager.setFallbackToNoOpCache(true); + return cacheManager; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsannotatedsupport/JmsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsannotatedsupport/JmsConfiguration.java new file mode 100644 index 000000000000..932988ce632a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsannotatedsupport/JmsConfiguration.java @@ -0,0 +1,45 @@ +/* + * 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.jms.jmsannotatedsupport; + + +import jakarta.jms.ConnectionFactory; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; +import org.springframework.jms.support.destination.DestinationResolver; + +// tag::snippet[] +@Configuration +@EnableJms +public class JmsConfiguration { + + @Bean + public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, + DestinationResolver destinationResolver) { + + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory); + factory.setDestinationResolver(destinationResolver); + factory.setSessionTransacted(true); + factory.setConcurrency("3-10"); + return factory; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/AlternativeJmsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/AlternativeJmsConfiguration.java new file mode 100644 index 000000000000..dc183df8eb3a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/AlternativeJmsConfiguration.java @@ -0,0 +1,46 @@ +/* + * 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.jms.jmsjcamessageendpointmanager; + +import jakarta.jms.MessageListener; +import jakarta.resource.spi.ResourceAdapter; +import org.apache.activemq.ra.ActiveMQActivationSpec; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager; + +@Configuration +public class AlternativeJmsConfiguration { + + // tag::snippet[] + @Bean + JmsMessageEndpointManager jmsMessageEndpointManager(ResourceAdapter resourceAdapter, + MessageListener myMessageListener) { + + ActiveMQActivationSpec spec = new ActiveMQActivationSpec(); + spec.setDestination("myQueue"); + spec.setDestinationType("jakarta.jms.Queue"); + + JmsMessageEndpointManager endpointManager = new JmsMessageEndpointManager(); + endpointManager.setResourceAdapter(resourceAdapter); + endpointManager.setActivationSpec(spec); + endpointManager.setMessageListener(myMessageListener); + return endpointManager; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/JmsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/JmsConfiguration.java new file mode 100644 index 000000000000..a95e1c8aafe3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/JmsConfiguration.java @@ -0,0 +1,46 @@ +/* + * 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.jms.jmsjcamessageendpointmanager; + + +import jakarta.jms.MessageListener; +import jakarta.resource.spi.ResourceAdapter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.listener.endpoint.JmsActivationSpecConfig; +import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager; + +@Configuration +public class JmsConfiguration { + + // tag::snippet[] + @Bean + public JmsMessageEndpointManager jmsMessageEndpointManager(ResourceAdapter resourceAdapter, + MessageListener myMessageListener) { + + JmsActivationSpecConfig specConfig = new JmsActivationSpecConfig(); + specConfig.setDestinationName("myQueue"); + + JmsMessageEndpointManager endpointManager = new JmsMessageEndpointManager(); + endpointManager.setResourceAdapter(resourceAdapter); + endpointManager.setActivationSpecConfig(specConfig); + endpointManager.setMessageListener(myMessageListener); + return endpointManager; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasync/ExampleListener.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasync/ExampleListener.java new file mode 100644 index 000000000000..d535f8cb0811 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasync/ExampleListener.java @@ -0,0 +1,41 @@ +/* + * 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.jms.jmsreceivingasync; + +import jakarta.jms.JMSException; +import jakarta.jms.Message; +import jakarta.jms.MessageListener; +import jakarta.jms.TextMessage; + +// tag::snippet[] +public class ExampleListener implements MessageListener { + + public void onMessage(Message message) { + if (message instanceof TextMessage textMessage) { + try { + System.out.println(textMessage.getText()); + } + catch (JMSException ex) { + throw new RuntimeException(ex); + } + } + else { + throw new IllegalArgumentException("Message must be of type TextMessage"); + } + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasync/JmsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasync/JmsConfiguration.java new file mode 100644 index 000000000000..6f9fb309ae25 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasync/JmsConfiguration.java @@ -0,0 +1,46 @@ +/* + * 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.jms.jmsreceivingasync; + +import jakarta.jms.ConnectionFactory; +import jakarta.jms.Destination; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.listener.DefaultMessageListenerContainer; + +@Configuration +public class JmsConfiguration { + + // tag::snippet[] + @Bean + ExampleListener messageListener() { + return new ExampleListener(); + } + + @Bean + DefaultMessageListenerContainer jmsContainer(ConnectionFactory connectionFactory, Destination destination, + ExampleListener messageListener) { + + DefaultMessageListenerContainer jmsContainer = new DefaultMessageListenerContainer(); + jmsContainer.setConnectionFactory(connectionFactory); + jmsContainer.setDestination(destination); + jmsContainer.setMessageListener(messageListener); + return jmsContainer; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultMessageDelegate.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultMessageDelegate.java new file mode 100644 index 000000000000..acad7cbcc248 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultMessageDelegate.java @@ -0,0 +1,45 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter; + +import java.io.Serializable; +import java.util.Map; + +// tag::snippet[] +public class DefaultMessageDelegate implements MessageDelegate { + + @Override + public void handleMessage(String message) { + // ... + } + + @Override + public void handleMessage(Map message) { + // ... + } + + @Override + public void handleMessage(byte[] message) { + // ... + } + + @Override + public void handleMessage(Serializable message) { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultResponsiveTextMessageDelegate.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultResponsiveTextMessageDelegate.java new file mode 100644 index 000000000000..bfa2e6168455 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultResponsiveTextMessageDelegate.java @@ -0,0 +1,29 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter; + +import jakarta.jms.TextMessage; + +// tag::snippet[] +public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate { + + @Override + public String receive(TextMessage message) { + return "message"; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultTextMessageDelegate.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultTextMessageDelegate.java new file mode 100644 index 000000000000..00d32876ac26 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultTextMessageDelegate.java @@ -0,0 +1,29 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter; + +import org.springframework.web.socket.TextMessage; + +// tag::snippet[] +public class DefaultTextMessageDelegate implements TextMessageDelegate { + + @Override + public void receive(TextMessage message) { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/JmsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/JmsConfiguration.java new file mode 100644 index 000000000000..6bb0bf75cd1e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/JmsConfiguration.java @@ -0,0 +1,48 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter; + +import jakarta.jms.ConnectionFactory; +import jakarta.jms.Destination; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.docs.integration.jms.jmsreceivingasync.ExampleListener; +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.adapter.MessageListenerAdapter; + +@Configuration +public class JmsConfiguration { + + // tag::snippet[] + @Bean + MessageListenerAdapter messageListener(DefaultMessageDelegate messageDelegate) { + return new MessageListenerAdapter(messageDelegate); + } + + @Bean + DefaultMessageListenerContainer jmsContainer(ConnectionFactory connectionFactory, Destination destination, + ExampleListener messageListener) { + + DefaultMessageListenerContainer jmsContainer = new DefaultMessageListenerContainer(); + jmsContainer.setConnectionFactory(connectionFactory); + jmsContainer.setDestination(destination); + jmsContainer.setMessageListener(messageListener); + return jmsContainer; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageDelegate.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageDelegate.java new file mode 100644 index 000000000000..f15b002ff2f1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageDelegate.java @@ -0,0 +1,33 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter; + +import java.io.Serializable; +import java.util.Map; + +// tag::snippet[] +public interface MessageDelegate { + + void handleMessage(String message); + + void handleMessage(Map message); + + void handleMessage(byte[] message); + + void handleMessage(Serializable message); +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageListenerConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageListenerConfiguration.java new file mode 100644 index 000000000000..565e2dffe17f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageListenerConfiguration.java @@ -0,0 +1,36 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.listener.adapter.MessageListenerAdapter; + +@Configuration +public class MessageListenerConfiguration { + + // tag::snippet[] + @Bean + MessageListenerAdapter messageListener(DefaultTextMessageDelegate messageDelegate) { + MessageListenerAdapter messageListener = new MessageListenerAdapter(messageDelegate); + messageListener.setDefaultListenerMethod("receive"); + // We don't want automatic message context extraction + messageListener.setMessageConverter(null); + return messageListener; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/ResponsiveTextMessageDelegate.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/ResponsiveTextMessageDelegate.java new file mode 100644 index 000000000000..ae22b4838fce --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/ResponsiveTextMessageDelegate.java @@ -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.integration.jms.jmsreceivingasyncmessagelisteneradapter; + +import jakarta.jms.TextMessage; + +// tag::snippet[] +public interface ResponsiveTextMessageDelegate { + + // Notice the return type... + String receive(TextMessage message); +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/TextMessageDelegate.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/TextMessageDelegate.java new file mode 100644 index 000000000000..ecd0f3007dc8 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/TextMessageDelegate.java @@ -0,0 +1,26 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter; + +import org.springframework.web.socket.TextMessage; + +// tag::snippet[] +public interface TextMessageDelegate { + + void receive(TextMessage message); +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmstxparticipation/ExternalTxJmsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmstxparticipation/ExternalTxJmsConfiguration.java new file mode 100644 index 000000000000..cd10a82ac930 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmstxparticipation/ExternalTxJmsConfiguration.java @@ -0,0 +1,51 @@ +/* + * 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.jms.jmstxparticipation; + +import jakarta.jms.ConnectionFactory; +import jakarta.jms.Destination; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.docs.integration.jms.jmsreceivingasync.ExampleListener; +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.transaction.jta.JtaTransactionManager; + +@Configuration +public class ExternalTxJmsConfiguration { + + // tag::transactionManagerSnippet[] + @Bean + JtaTransactionManager transactionManager() { + return new JtaTransactionManager(); + } + // end::transactionManagerSnippet[] + + // tag::jmsContainerSnippet[] + @Bean + DefaultMessageListenerContainer jmsContainer(ConnectionFactory connectionFactory, Destination destination, + ExampleListener messageListener) { + + DefaultMessageListenerContainer jmsContainer = new DefaultMessageListenerContainer(); + jmsContainer.setConnectionFactory(connectionFactory); + jmsContainer.setDestination(destination); + jmsContainer.setMessageListener(messageListener); + jmsContainer.setSessionTransacted(true); + return jmsContainer; + } + // end::jmsContainerSnippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmstxparticipation/JmsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmstxparticipation/JmsConfiguration.java new file mode 100644 index 000000000000..bdc8c847e2e3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmstxparticipation/JmsConfiguration.java @@ -0,0 +1,43 @@ +/* + * 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.jms.jmstxparticipation; + +import jakarta.jms.ConnectionFactory; +import jakarta.jms.Destination; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.docs.integration.jms.jmsreceivingasync.ExampleListener; +import org.springframework.jms.listener.DefaultMessageListenerContainer; + +@Configuration +public class JmsConfiguration { + + // tag::snippet[] + @Bean + DefaultMessageListenerContainer jmsContainer(ConnectionFactory connectionFactory, Destination destination, + ExampleListener messageListener) { + + DefaultMessageListenerContainer jmsContainer = new DefaultMessageListenerContainer(); + jmsContainer.setConnectionFactory(connectionFactory); + jmsContainer.setDestination(destination); + jmsContainer.setMessageListener(messageListener); + jmsContainer.setSessionTransacted(true); + return jmsContainer; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/CustomJmxConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/CustomJmxConfiguration.java new file mode 100644 index 000000000000..d227fba9dbb3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/CustomJmxConfiguration.java @@ -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.integration.jmx.jmxcontextmbeanexport; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableMBeanExport; + +// tag::snippet[] +@Configuration +@EnableMBeanExport(server="myMBeanServer", defaultDomain="myDomain") +public class CustomJmxConfiguration { +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/JmxConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/JmxConfiguration.java new file mode 100644 index 000000000000..70f3a08667c2 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/JmxConfiguration.java @@ -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.integration.jmx.jmxcontextmbeanexport; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableMBeanExport; + +// tag::snippet[] +@Configuration +@EnableMBeanExport +public class JmxConfiguration { +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.java b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.java new file mode 100644 index 000000000000..4100da2d922f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.jmx.jmxexporting; + +public interface IJmxTestBean { + + int getAge(); + + void setAge(int age); + + void setName(String name); + + String getName(); + + int add(int x, int y); + + void dontExposeMe(); +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxexporting/JmxConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxexporting/JmxConfiguration.java new file mode 100644 index 000000000000..98ce1302a694 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxexporting/JmxConfiguration.java @@ -0,0 +1,44 @@ +/* + * 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.jmx.jmxexporting; + +import java.util.Map; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jmx.export.MBeanExporter; + +// tag::snippet[] +@Configuration +public class JmxConfiguration { + + @Bean + MBeanExporter exporter(JmxTestBean testBean) { + MBeanExporter exporter = new MBeanExporter(); + exporter.setBeans(Map.of("bean:name=testBean1", testBean)); + return exporter; + } + + @Bean + JmxTestBean testBean() { + JmxTestBean testBean = new JmxTestBean(); + testBean.setName("TEST"); + testBean.setAge(100); + return testBean; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.java b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.java new file mode 100644 index 000000000000..6412ecabf99a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.java @@ -0,0 +1,55 @@ +/* + * 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.jmx.jmxexporting; + +// tag::snippet[] +public class JmxTestBean implements IJmxTestBean { + + private String name; + private int age; + + @Override + public int getAge() { + return age; + } + + @Override + public void setAge(int age) { + this.age = age; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public int add(int x, int y) { + return x + y; + } + + @Override + public void dontExposeMe() { + throw new RuntimeException(); + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/mailusage/OrderManager.java b/framework-docs/src/main/java/org/springframework/docs/integration/mailusage/OrderManager.java new file mode 100644 index 000000000000..b1dab51d7925 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/mailusage/OrderManager.java @@ -0,0 +1,26 @@ +/* + * 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.mailusage; + +import org.springframework.docs.integration.mailusagesimple.Order; + +// tag::snippet[] +public interface OrderManager { + + void placeOrder(Order order); +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/Customer.java b/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/Customer.java new file mode 100644 index 000000000000..0d01074c74b6 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/Customer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.mailusagesimple; + +public class Customer { + + public String getEmailAddress() { + return null; + } + + public String getFirstName() { + return null; + } + + public String getLastName() { + return null; + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/MailConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/MailConfiguration.java new file mode 100644 index 000000000000..723340868d4e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/MailConfiguration.java @@ -0,0 +1,52 @@ +/* + * 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; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +@Configuration +public class MailConfiguration { + + // tag::snippet[] + @Bean + JavaMailSender mailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost("mail.mycompany.example"); + return mailSender; + } + + @Bean // this is a template message that we can pre-load with default state + SimpleMailMessage templateMessage() { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom("customerservice@mycompany.example"); + message.setSubject("Your order"); + return message; + } + + @Bean + SimpleOrderManager orderManager(JavaMailSender mailSender, SimpleMailMessage templateMessage) { + SimpleOrderManager orderManager = new SimpleOrderManager(); + orderManager.setMailSender(mailSender); + orderManager.setTemplateMessage(templateMessage); + return orderManager; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/Order.java b/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/Order.java new file mode 100644 index 000000000000..27c53e0d6a53 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/Order.java @@ -0,0 +1,28 @@ +/* + * 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; + +public class Order { + + public Customer getCustomer() { + return null; + } + + public String getOrderNumber() { + return null; + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/SimpleOrderManager.java b/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/SimpleOrderManager.java new file mode 100644 index 000000000000..6835d2f0afe0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/mailusagesimple/SimpleOrderManager.java @@ -0,0 +1,64 @@ +/* + * 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; + +import org.springframework.docs.integration.mailusage.OrderManager; +import org.springframework.mail.SimpleMailMessage; + +import org.springframework.mail.MailException; +import org.springframework.mail.MailSender; + +// tag::snippet[] +public class SimpleOrderManager implements OrderManager { + + private MailSender mailSender; + private SimpleMailMessage templateMessage; + + public void setMailSender(MailSender mailSender) { + this.mailSender = mailSender; + } + + public void setTemplateMessage(SimpleMailMessage templateMessage) { + this.templateMessage = templateMessage; + } + + @Override + public void placeOrder(Order order) { + + // Do the business calculations... + + // Call the collaborators to persist the order... + + // Create a thread-safe "copy" of the template message and customize it + SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage); + msg.setTo(order.getCustomer().getEmailAddress()); + msg.setText( + "Dear " + order.getCustomer().getFirstName() + + order.getCustomer().getLastName() + + ", thank you for placing order. Your order number is " + + order.getOrderNumber()); + try { + this.mailSender.send(msg); + } + catch (MailException ex) { + // simply log it and go on... + System.err.println(ex.getMessage()); + } + } + +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/CustomServerRequestObservationConvention.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/CustomServerRequestObservationConvention.java index 44000cd41bf9..b31d4e57c70b 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/CustomServerRequestObservationConvention.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/CustomServerRequestObservationConvention.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.docs.integration.observability.config.conventions; +import java.util.Locale; + import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; @@ -34,7 +36,7 @@ public String getName() { @Override public String getContextualName(ServerRequestObservationContext context) { // will be used for the trace name - return "http " + context.getCarrier().getMethod().toLowerCase(); + return "http " + context.getCarrier().getMethod().toLowerCase(Locale.ROOT); } @Override diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.java b/framework-docs/src/main/java/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.java new file mode 100644 index 000000000000..8fd2a12ee077 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.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.docs.integration.resthttpinterface.customresolver; + +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.invoker.HttpRequestValues; +import org.springframework.web.service.invoker.HttpServiceArgumentResolver; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +public class CustomHttpServiceArgumentResolver { + + // tag::httpinterface[] + public interface RepositoryService { + + @GetExchange("/repos/search") + List searchRepository(Search search); + + } + // end::httpinterface[] + + class Sample { + + void sample() { + // tag::usage[] + RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build(); + RestClientAdapter adapter = RestClientAdapter.create(restClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory + .builderFor(adapter) + .customArgumentResolver(new SearchQueryArgumentResolver()) + .build(); + RepositoryService repositoryService = factory.createClient(RepositoryService.class); + + Search search = Search.create() + .owner("spring-projects") + .language("java") + .query("rest") + .build(); + List repositories = repositoryService.searchRepository(search); + // end::usage[] + } + + } + + // tag::argumentresolver[] + static class SearchQueryArgumentResolver implements HttpServiceArgumentResolver { + @Override + public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) { + if (parameter.getParameterType().equals(Search.class)) { + Search search = (Search) argument; + requestValues.addRequestParameter("owner", search.owner()); + requestValues.addRequestParameter("language", search.language()); + requestValues.addRequestParameter("query", search.query()); + return true; + } + return false; + } + } + // end::argumentresolver[] + + + record Search (String query, String owner, String language) { + + static Builder create() { + return new Builder(); + } + + static class Builder { + + Builder query(String query) { return this;} + + Builder owner(String owner) { return this;} + + Builder language(String language) { return this;} + + Search build() { + return new Search(null, null, null); + } + } + + } + + record Repository(String name) { + + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/schedulingenableannotationsupport/SchedulingConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/schedulingenableannotationsupport/SchedulingConfiguration.java new file mode 100644 index 000000000000..d38b7b4b4c9d --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/schedulingenableannotationsupport/SchedulingConfiguration.java @@ -0,0 +1,29 @@ +/* + * 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.schedulingenableannotationsupport; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +// tag::snippet[] +@Configuration +@EnableAsync +@EnableScheduling +public class SchedulingConfiguration { +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/LoggingTaskDecorator.java b/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/LoggingTaskDecorator.java new file mode 100644 index 000000000000..2f92fff0e4c5 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/LoggingTaskDecorator.java @@ -0,0 +1,36 @@ +/* + * 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.schedulingtaskexecutorusage; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.task.TaskDecorator; + +public class LoggingTaskDecorator implements TaskDecorator { + + private static final Log logger = LogFactory.getLog(LoggingTaskDecorator.class); + + @Override + public Runnable decorate(Runnable runnable) { + return () -> { + logger.debug("Before execution of " + runnable); + runnable.run(); + logger.debug("After execution of " + runnable); + }; + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorConfiguration.java new file mode 100644 index 000000000000..d5dd6ceab451 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorConfiguration.java @@ -0,0 +1,50 @@ +/* + * 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.schedulingtaskexecutorusage; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +public class TaskExecutorConfiguration { + + // tag::snippet[] + @Bean + ThreadPoolTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(5); + taskExecutor.setMaxPoolSize(10); + taskExecutor.setQueueCapacity(25); + return taskExecutor; + } + + @Bean + TaskExecutorExample taskExecutorExample(ThreadPoolTaskExecutor taskExecutor) { + return new TaskExecutorExample(taskExecutor); + } + // end::snippet[] + + // tag::decorator[] + @Bean + ThreadPoolTaskExecutor decoratedTaskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setTaskDecorator(new LoggingTaskDecorator()); + return taskExecutor; + } + // end::decorator[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorExample.java b/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorExample.java new file mode 100644 index 000000000000..62a156cff177 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorExample.java @@ -0,0 +1,50 @@ +/* + * 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.schedulingtaskexecutorusage; + +import org.springframework.core.task.TaskExecutor; + +// tag::snippet[] +public class TaskExecutorExample { + + private class MessagePrinterTask implements Runnable { + + private String message; + + public MessagePrinterTask(String message) { + this.message = message; + } + + public void run() { + System.out.println(message); + } + } + + private TaskExecutor taskExecutor; + + public TaskExecutorExample(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + public void printMessages() { + for(int i = 0; i < 25; i++) { + taskExecutor.execute(new MessagePrinterTask("Message" + i)); + } + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertions/HotelControllerTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertions/HotelControllerTests.java new file mode 100644 index 000000000000..647cb2099db9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertions/HotelControllerTests.java @@ -0,0 +1,50 @@ +/* + * 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.mockmvctesterassertions; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.assertj.MockMvcTester; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HotelControllerTests { + + private final MockMvcTester mockMvc = MockMvcTester.of(new HotelController()); + + + void getHotel() { + // tag::get[] + assertThat(mockMvc.get().uri("/hotels/{id}", 42)) + .hasStatusOk() + .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .bodyJson().isLenientlyEqualTo("sample/hotel-42.json"); + // end::get[] + } + + + void getHotelInvalid() { + // tag::failure[] + assertThat(mockMvc.get().uri("/hotels/{id}", -1)) + .hasFailed() + .hasStatus(HttpStatus.BAD_REQUEST) + .failure().hasMessageContaining("Identifier should be positive"); + // end::failure[] + } + + static class HotelController {} +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertionsjson/FamilyControllerTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertionsjson/FamilyControllerTests.java new file mode 100644 index 000000000000..96d41a2ea8e4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertionsjson/FamilyControllerTests.java @@ -0,0 +1,69 @@ +/* + * 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.mockmvctesterassertionsjson; + +import org.assertj.core.api.InstanceOfAssertFactories; + +import org.springframework.test.web.servlet.assertj.MockMvcTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +class FamilyControllerTests { + + private final MockMvcTester mockMvc = MockMvcTester.of(new FamilyController()); + + + void extractingPathAsMap() { + // tag::extract-asmap[] + assertThat(mockMvc.get().uri("/family")).bodyJson() + .extractingPath("$.members[0]") + .asMap() + .contains(entry("name", "Homer")); + // end::extract-asmap[] + } + + void extractingPathAndConvertWithType() { + // tag::extract-convert[] + assertThat(mockMvc.get().uri("/family")).bodyJson() + .extractingPath("$.members[0]") + .convertTo(Member.class) + .satisfies(member -> assertThat(member.name).isEqualTo("Homer")); + // end::extract-convert[] + } + + void extractingPathAndConvertWithAssertFactory() { + // tag::extract-convert-assert-factory[] + assertThat(mockMvc.get().uri("/family")).bodyJson() + .extractingPath("$.members") + .convertTo(InstanceOfAssertFactories.list(Member.class)) + .hasSize(5) + .element(0).satisfies(member -> assertThat(member.name).isEqualTo("Homer")); + // end::extract-convert-assert-factory[] + } + + void assertTheSimpsons() { + // tag::assert-file[] + assertThat(mockMvc.get().uri("/family")).bodyJson() + .isStrictlyEqualTo("sample/simpsons.json"); + // end::assert-file[] + } + + static class FamilyController {} + + record Member(String name) {} +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterintegration/HotelControllerTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterintegration/HotelControllerTests.java new file mode 100644 index 000000000000..cc1e6b1c4457 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterintegration/HotelControllerTests.java @@ -0,0 +1,47 @@ +/* + * 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.mockmvctesterintegration; + +import org.springframework.test.web.servlet.assertj.MockMvcTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class HotelControllerTests { + + private final MockMvcTester mockMvc = MockMvcTester.of(new HotelController()); + + + void perform() { + // tag::perform[] + // Static import on MockMvcRequestBuilders.get + assertThat(mockMvc.perform(get("/hotels/{id}", 42))) + .hasStatusOk(); + // end::perform[] + } + + void performWithCustomMatcher() { + // tag::matches[] + // Static import on MockMvcResultMatchers.status + assertThat(mockMvc.get().uri("/hotels/{id}", 42)) + .matches(status().isOk()); + // end::matches[] + } + + static class HotelController {} +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequests/HotelControllerTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequests/HotelControllerTests.java new file mode 100644 index 000000000000..b72da0454c8e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequests/HotelControllerTests.java @@ -0,0 +1,68 @@ +/* + * 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.mockmvctesterrequests; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.assertj.MockMvcTester; +import org.springframework.test.web.servlet.assertj.MvcTestResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +/** + * @author Stephane Nicoll + */ +public class HotelControllerTests { + + private final MockMvcTester mockMvc = MockMvcTester.of(new HotelController()); + + + void createHotel() { + // tag::post[] + assertThat(mockMvc.post().uri("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON)) + . // ... + // end::post[] + hasStatusOk(); + } + + void createHotelMultipleAssertions() { + // tag::post-exchange[] + MvcTestResult result = mockMvc.post().uri("/hotels/{id}", 42) + .accept(MediaType.APPLICATION_JSON).exchange(); + assertThat(result). // ... + // end::post-exchange[] + hasStatusOk(); + } + + void queryParameters() { + // tag::query-parameters[] + assertThat(mockMvc.get().uri("/hotels?thing={thing}", "somewhere")) + . // ... + // end::query-parameters[] + hasStatusOk(); + } + + void parameters() { + // tag::parameters[] + assertThat(mockMvc.get().uri("/hotels").param("thing", "somewhere")) + . // ... + // end::parameters[] + hasStatusOk(); + } + + static class HotelController {} +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsasync/AsyncControllerTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsasync/AsyncControllerTests.java new file mode 100644 index 000000000000..fa13d3abdff9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsasync/AsyncControllerTests.java @@ -0,0 +1,38 @@ +/* + * 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.mockmvctesterrequestsasync; + +import java.time.Duration; + +import org.springframework.test.web.servlet.assertj.MockMvcTester; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AsyncControllerTests { + + private final MockMvcTester mockMvc = MockMvcTester.of(new AsyncController()); + + void asyncExchangeWithCustomTimeToWait() { + // tag::duration[] + assertThat(mockMvc.get().uri("/compute").exchange(Duration.ofSeconds(5))) + . // ... + // end::duration[] + hasStatusOk(); + } + + static class AsyncController {} +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsmultipart/MultipartControllerTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsmultipart/MultipartControllerTests.java new file mode 100644 index 000000000000..82371dd15cce --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsmultipart/MultipartControllerTests.java @@ -0,0 +1,40 @@ +/* + * 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.mockmvctesterrequestsmultipart; + +import java.nio.charset.StandardCharsets; + +import org.springframework.test.web.servlet.assertj.MockMvcTester; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MultipartControllerTests { + + private final MockMvcTester mockMvc = MockMvcTester.of(new MultipartController()); + + void multiPart() { + // tag::snippet[] + assertThat(mockMvc.post().uri("/upload").multipart() + .file("file1.txt", "Hello".getBytes(StandardCharsets.UTF_8)) + .file("file2.txt", "World".getBytes(StandardCharsets.UTF_8))) + . // ... + // end::snippet[] + hasStatusOk(); + } + + static class MultipartController {} +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestspaths/HotelControllerTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestspaths/HotelControllerTests.java new file mode 100644 index 000000000000..da39047300e2 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestspaths/HotelControllerTests.java @@ -0,0 +1,54 @@ +/* + * 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.mockmvctesterrequestspaths; + +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.assertj.MockMvcTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +/** + * @author Stephane Nicoll + */ +public class HotelControllerTests { + + private final MockMvcTester mockMvc = MockMvcTester.of(new HotelController()); + + void contextAndServletPaths() { + // tag::context-servlet-paths[] + assertThat(mockMvc.get().uri("/app/main/hotels/{id}", 42) + .contextPath("/app").servletPath("/main")) + . // ... + // end::context-servlet-paths[] + hasStatusOk(); + } + + void configureMockMvcTesterWithDefaultSettings() { + // tag::default-customizations[] + MockMvcTester mockMvc = MockMvcTester.of(List.of(new HotelController()), + builder -> builder.defaultRequest(get("/") + .contextPath("/app").servletPath("/main") + .accept(MediaType.APPLICATION_JSON)).build()); + // end::default-customizations[] + } + + + static class HotelController {} +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountController.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountController.java new file mode 100644 index 000000000000..4a71ac3e4364 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountController.java @@ -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.web.bind.annotation.RestController; + +@RestController +public class AccountController { + + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerIntegrationTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerIntegrationTests.java new file mode 100644 index 000000000000..0ec02321f2f0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerIntegrationTests.java @@ -0,0 +1,37 @@ +/* + * 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.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.test.web.servlet.assertj.MockMvcTester; +import org.springframework.web.context.WebApplicationContext; + +// tag::snippet[] +@SpringJUnitWebConfig(ApplicationWebConfiguration.class) +class AccountControllerIntegrationTests { + + private final MockMvcTester mockMvc; + + AccountControllerIntegrationTests(@Autowired WebApplicationContext wac) { + this.mockMvc = MockMvcTester.from(wac); + } + + // ... + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerStandaloneTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerStandaloneTests.java new file mode 100644 index 000000000000..d14766d88696 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerStandaloneTests.java @@ -0,0 +1,29 @@ +/* + * 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.test.web.servlet.assertj.MockMvcTester; + +// tag::snippet[] +public class AccountControllerStandaloneTests { + + private final MockMvcTester mockMvc = MockMvcTester.of(new AccountController()); + + // ... + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/ApplicationWebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/ApplicationWebConfiguration.java new file mode 100644 index 000000000000..7d9b7edebf08 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/ApplicationWebConfiguration.java @@ -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 +@EnableWebMvc +public class ApplicationWebConfiguration { +} 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 new file mode 100644 index 000000000000..63326e903aa4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.java @@ -0,0 +1,42 @@ +/* + * 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.converter; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.ApplicationWebConfiguration; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.test.web.servlet.assertj.MockMvcTester; +import org.springframework.web.context.WebApplicationContext; + +// tag::snippet[] +@SpringJUnitWebConfig(ApplicationWebConfiguration.class) +class AccountControllerIntegrationTests { + + private final MockMvcTester mockMvc; + + AccountControllerIntegrationTests(@Autowired WebApplicationContext wac) { + this.mockMvc = MockMvcTester.from(wac).withHttpMessageConverters( + List.of(wac.getBean(AbstractJackson2HttpMessageConverter.class))); + } + + // ... + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/webfluxanncontrollerexceptions/SimpleController.java b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/webfluxanncontrollerexceptions/SimpleController.java new file mode 100644 index 000000000000..423f47a567a6 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/webfluxanncontrollerexceptions/SimpleController.java @@ -0,0 +1,33 @@ +/* + * 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.webflux.controller.webfluxanncontrollerexceptions; + +import java.io.IOException; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Controller +public class SimpleController { + + @ExceptionHandler(IOException.class) + public ResponseEntity handle() { + return ResponseEntity.internalServerError().body("Could not read file storage"); + } + +} \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/webfluxannexceptionhandlermedia/MediaTypeController.java b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/webfluxannexceptionhandlermedia/MediaTypeController.java new file mode 100644 index 000000000000..08e700360441 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/webfluxannexceptionhandlermedia/MediaTypeController.java @@ -0,0 +1,43 @@ +/* + * 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.webflux.controller.webfluxannexceptionhandlermedia; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Controller +public class MediaTypeController { + + // tag::mediatype[] + @ExceptionHandler(produces = "application/json") + public ResponseEntity handleJson(IllegalArgumentException exc) { + return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42)); + } + + @ExceptionHandler(produces = "text/html") + public String handle(IllegalArgumentException exc, Model model) { + model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42)); + return "errorView"; + } + // end::mediatype[] + + static record ErrorMessage(String message, int code) { + + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webflux/filters/urlhandler/UrlHandlerFilterConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webflux/filters/urlhandler/UrlHandlerFilterConfiguration.java new file mode 100644 index 000000000000..d8fbd6864980 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/filters/urlhandler/UrlHandlerFilterConfiguration.java @@ -0,0 +1,34 @@ +/* + * 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.webflux.filters.urlhandler; + +import org.springframework.http.HttpStatus; +import org.springframework.web.filter.reactive.UrlHandlerFilter; + +public class UrlHandlerFilterConfiguration { + + public void configureUrlHandlerFilter() { + // tag::config[] + UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter + // will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post" + .trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT) + // will mutate the request to "/admin/user/account/" and make it as "/admin/user/account" + .trailingSlashHandler("/admin/**").mutateRequest() + .build(); + // end::config[] + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.java b/framework-docs/src/main/java/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.java new file mode 100644 index 000000000000..ddd53b3683eb --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.java @@ -0,0 +1,33 @@ +/* + * 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.webflux.webfluxconfigpathmatching; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerTypePredicate; +import org.springframework.web.reactive.config.PathMatchConfigurer; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +@Configuration +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configurePathMatching(PathMatchConfigurer configurer) { + configurer.addPathPrefix( + "/api", HandlerTypePredicate.forAnnotation(RestController.class)); + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/filters/urlhandler/UrlHandlerFilterConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/filters/urlhandler/UrlHandlerFilterConfiguration.java new file mode 100644 index 000000000000..ba1a4408d9c4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/filters/urlhandler/UrlHandlerFilterConfiguration.java @@ -0,0 +1,34 @@ +/* + * 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.filters.urlhandler; + +import org.springframework.http.HttpStatus; +import org.springframework.web.filter.UrlHandlerFilter; + +public class UrlHandlerFilterConfiguration { + + public void configureUrlHandlerFilter() { + // tag::config[] + UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter + // will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post" + .trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT) + // will wrap the request to "/admin/user/account/" and make it as "/admin/user/account" + .trailingSlashHandler("/admin/**").wrapRequest() + .build(); + // end::config[] + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.java new file mode 100644 index 000000000000..f6f386efc17b --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.java @@ -0,0 +1,28 @@ +/* + * 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.mvcconfigadvancedjava; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; + +// tag::snippet[] +@Configuration +public class WebConfiguration extends DelegatingWebMvcConfiguration { + + // ... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java new file mode 100644 index 000000000000..f3649e9accdb --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigadvancedxml; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.stereotype.Component; + +// tag::snippet[] +@Component +public class MyPostProcessor implements BeanPostProcessor { + + public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { + // ... + return bean; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.java new file mode 100644 index 000000000000..9697847decbc --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.java @@ -0,0 +1,34 @@ +/* + * 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.mvcconfigcontentnegotiation; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.mediaType("json", MediaType.APPLICATION_JSON); + configurer.mediaType("xml", MediaType.APPLICATION_XML); + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.java new file mode 100644 index 000000000000..efa8b5090ad9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.java @@ -0,0 +1,35 @@ +/* + * 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.mvcconfigconversion; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class DateTimeWebConfiguration implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(registry); + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.java new file mode 100644 index 000000000000..ec7166f54f60 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigconversion; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + // ... + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.java new file mode 100644 index 000000000000..f6aee7304608 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.java @@ -0,0 +1,28 @@ +/* + * 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.mvcconfigcustomize; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + // Implement configuration methods... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.java new file mode 100644 index 000000000000..8616dd994fbb --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.java @@ -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.mvcconfigenable; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +// tag::snippet[] +@Configuration +@EnableWebMvc +public class WebConfiguration { +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.java new file mode 100644 index 000000000000..e23168d9b191 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.java @@ -0,0 +1,33 @@ +/* + * 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.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.i18n.LocaleChangeInterceptor; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new LocaleChangeInterceptor()); + } +} +// 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 new file mode 100644 index 000000000000..019a99a270cb --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java @@ -0,0 +1,45 @@ +/* + * 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.mvcconfigmessageconverters; + +import java.text.SimpleDateFormat; +import java.util.List; + +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureMessageConverters(List> converters) { + Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() + .indentOutput(true) + .dateFormat(new SimpleDateFormat("yyyy-MM-dd")) + .modulesToInstall(new ParameterNamesModule()); + converters.add(new MappingJackson2HttpMessageConverter(builder.build())); + converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java new file mode 100644 index 000000000000..f08527787ddb --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java @@ -0,0 +1,25 @@ +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigpathmatching; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerTypePredicate; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.util.pattern.PathPatternParser; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); + } + + private PathPatternParser patternParser() { + PathPatternParser pathPatternParser = new PathPatternParser(); + // ... + return pathPatternParser; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.java new file mode 100644 index 000000000000..10c07a4eab7c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.java @@ -0,0 +1,36 @@ +/* + * 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.mvcconfigstaticresources; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.VersionResourceResolver; + +// tag::snippet[] +@Configuration +public class VersionedConfiguration implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public/") + .resourceChain(true) + .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**")); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.java new file mode 100644 index 000000000000..46df090b4d13 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.java @@ -0,0 +1,37 @@ +/* + * 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.mvcconfigstaticresources; + +import java.time.Duration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.java new file mode 100644 index 000000000000..5fc1c723632f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigvalidation; + +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class FooValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return false; + } + + @Override + public void validate(Object target, Errors errors) { + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.java new file mode 100644 index 000000000000..0214a63ffcec --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigvalidation; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; + +// tag::snippet[] +@Controller +public class MyController { + + @InitBinder + public void initBinder(WebDataBinder binder) { + binder.addValidators(new FooValidator()); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java new file mode 100644 index 000000000000..5e8f46abc61e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java @@ -0,0 +1,35 @@ +/* + * 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.context.annotation.Configuration; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public Validator getValidator() { + Validator validator = new OptionalValidatorFactoryBean(); + // ... + return validator; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.java new file mode 100644 index 000000000000..f623fda64a6e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.java @@ -0,0 +1,33 @@ +/* + * 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.mvcconfigviewcontroller; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("home"); + } +} +// end::snippet[] + 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 new file mode 100644 index 000000000000..0d949748a323 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java @@ -0,0 +1,43 @@ +/* + * 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.mvcconfigviewresolvers; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; + +// tag::snippet[] +@Configuration +public class FreeMarkerConfiguration implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.enableContentNegotiation(new MappingJackson2JsonView()); + registry.freeMarker().cache(false); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/freemarker"); + return configurer; + } +} +// end::snippet[] \ No newline at end of file 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 new file mode 100644 index 000000000000..c4d27f555ebd --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java @@ -0,0 +1,34 @@ +/* + * 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.mvcconfigviewresolvers; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.enableContentNegotiation(new MappingJackson2JsonView()); + registry.jsp(); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.java new file mode 100644 index 000000000000..055263cac89c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcdefaultservlethandler; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class CustomDefaultServletConfiguration implements WebMvcConfigurer { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable("myCustomDefaultServlet"); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.java new file mode 100644 index 000000000000..49a110e68096 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcdefaultservlethandler; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.java new file mode 100644 index 000000000000..84aab1258cf4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.java @@ -0,0 +1,29 @@ +/* + * 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.mvccontroller.mvcanncontroller; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +// tag::snippet[] +@Configuration +@ComponentScan("org.example.web") +public class WebConfiguration { + + // ... +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandler/SimpleController.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandler/SimpleController.java new file mode 100644 index 000000000000..8da9c174e126 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandler/SimpleController.java @@ -0,0 +1,33 @@ +/* + * 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.mvccontroller.mvcannexceptionhandler; + +import java.io.IOException; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Controller +public class SimpleController { + + @ExceptionHandler(IOException.class) + public ResponseEntity handle() { + return ResponseEntity.internalServerError().body("Could not read file storage"); + } + +} \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlerexc/ExceptionController.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlerexc/ExceptionController.java new file mode 100644 index 000000000000..c11617638db1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlerexc/ExceptionController.java @@ -0,0 +1,45 @@ +/* + * 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.mvccontroller.mvcannexceptionhandlerexc; + +import java.io.IOException; +import java.nio.file.FileSystemException; +import java.rmi.RemoteException; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Controller +public class ExceptionController { + + // tag::narrow[] + @ExceptionHandler({FileSystemException.class, RemoteException.class}) + public ResponseEntity handleIoException(IOException ex) { + return ResponseEntity.internalServerError().body(ex.getMessage()); + } + // end::narrow[] + + + // tag::general[] + @ExceptionHandler({FileSystemException.class, RemoteException.class}) + public ResponseEntity handleExceptions(Exception ex) { + return ResponseEntity.internalServerError().body(ex.getMessage()); + } + // end::general[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlermedia/MediaTypeController.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlermedia/MediaTypeController.java new file mode 100644 index 000000000000..feecf5f67735 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlermedia/MediaTypeController.java @@ -0,0 +1,43 @@ +/* + * 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.mvccontroller.mvcannexceptionhandlermedia; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Controller +public class MediaTypeController { + + // tag::mediatype[] + @ExceptionHandler(produces = "application/json") + public ResponseEntity handleJson(IllegalArgumentException exc) { + return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42)); + } + + @ExceptionHandler(produces = "text/html") + public String handle(IllegalArgumentException exc, Model model) { + model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42)); + return "errorView"; + } + // end::mediatype[] + + static record ErrorMessage(String message, int code) { + + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompauthenticationtokenbased/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompauthenticationtokenbased/WebSocketConfiguration.java new file mode 100644 index 000000000000..2fb295375b2e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompauthenticationtokenbased/WebSocketConfiguration.java @@ -0,0 +1,49 @@ +/* + * 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.stomp.websocketstompauthenticationtokenbased; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + // Access authentication header(s) and invoke accessor.setUser(user) + } + return message; + } + }); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/MessageSizeLimitWebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/MessageSizeLimitWebSocketConfiguration.java new file mode 100644 index 000000000000..303e245f24f8 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/MessageSizeLimitWebSocketConfiguration.java @@ -0,0 +1,39 @@ +/* + * 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.stomp.websocketstompconfigurationperformance; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class MessageSizeLimitWebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registration) { + registration.setMessageSizeLimit(128 * 1024); + } + + // ... + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/WebSocketConfiguration.java new file mode 100644 index 000000000000..82d556dd59de --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/WebSocketConfiguration.java @@ -0,0 +1,39 @@ +/* + * 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.stomp.websocketstompconfigurationperformance; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registration) { + registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024); + } + + // ... + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/RedController.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/RedController.java new file mode 100644 index 000000000000..70f6ce808900 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/RedController.java @@ -0,0 +1,33 @@ +/* + * 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.stomp.websocketstompdestinationseparator; + +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.stereotype.Controller; + +// tag::snippet[] +@Controller +@MessageMapping("red") +public class RedController { + + @MessageMapping("blue.{green}") + public void handleGreen(@DestinationVariable String green) { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/WebSocketConfiguration.java new file mode 100644 index 000000000000..269f61bb800c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/WebSocketConfiguration.java @@ -0,0 +1,39 @@ +/* + * 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.stomp.websocketstompdestinationseparator; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + // ... + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setPathMatcher(new AntPathMatcher(".")); + registry.enableStompBrokerRelay("/queue", "/topic"); + registry.setApplicationDestinationPrefixes("/app"); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompenable/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompenable/WebSocketConfiguration.java new file mode 100644 index 000000000000..0b282736aaaa --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompenable/WebSocketConfiguration.java @@ -0,0 +1,47 @@ +/* + * 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.stomp.websocketstompenable; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // /portfolio is the HTTP URL for the endpoint to which a WebSocket (or SockJS) + // client needs to connect for the WebSocket handshake + registry.addEndpoint("/portfolio"); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + // STOMP messages whose destination header begins with /app are routed to + // @MessageMapping methods in @Controller classes + config.setApplicationDestinationPrefixes("/app"); + // Use the built-in message broker for subscriptions and broadcasting and + // route messages whose destination header begins with /topic or /queue to the broker + config.enableSimpleBroker("/topic", "/queue"); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelay/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelay/WebSocketConfiguration.java new file mode 100644 index 000000000000..20ddf32694f9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelay/WebSocketConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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.stomp.websocketstomphandlebrokerrelay; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/portfolio").withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableStompBrokerRelay("/topic", "/queue"); + registry.setApplicationDestinationPrefixes("/app"); + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelayconfigure/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelayconfigure/WebSocketConfiguration.java new file mode 100644 index 000000000000..eb1b255157a7 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelayconfigure/WebSocketConfiguration.java @@ -0,0 +1,47 @@ +/* + * 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.stomp.websocketstomphandlebrokerrelayconfigure; + +import java.net.InetSocketAddress; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompReactorNettyCodec; +import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + // ... + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient()); + registry.setApplicationDestinationPrefixes("/app"); + } + + private ReactorNettyTcpClient createTcpClient() { + return new ReactorNettyTcpClient<>( + client -> client.remoteAddress(() -> new InetSocketAddress(0)), + new StompReactorNettyCodec()); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomphandlesimplebroker/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomphandlesimplebroker/WebSocketConfiguration.java new file mode 100644 index 000000000000..b6518257ce66 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomphandlesimplebroker/WebSocketConfiguration.java @@ -0,0 +1,49 @@ +/* + * 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.stomp.websocketstomphandlesimplebroker; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + private TaskScheduler messageBrokerTaskScheduler; + + @Autowired + public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) { + this.messageBrokerTaskScheduler = taskScheduler; + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/queue/", "/topic/") + .setHeartbeatValue(new long[] {10000, 20000}) + .setTaskScheduler(this.messageBrokerTaskScheduler); + + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/MyChannelInterceptor.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/MyChannelInterceptor.java new file mode 100644 index 000000000000..721d631301ab --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/MyChannelInterceptor.java @@ -0,0 +1,36 @@ +/* + * 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.stomp.websocketstompinterceptors; + +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; + +// tag::snippet[] +public class MyChannelInterceptor implements ChannelInterceptor { + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + StompCommand command = accessor.getCommand(); + // ... + return message; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/WebSocketConfiguration.java new file mode 100644 index 000000000000..31082f7991d2 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/WebSocketConfiguration.java @@ -0,0 +1,34 @@ +/* + * 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.stomp.websocketstompinterceptors; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new MyChannelInterceptor()); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/GreetingController.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/GreetingController.java new file mode 100644 index 000000000000..9f8302ac11f2 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/GreetingController.java @@ -0,0 +1,44 @@ +/* + * 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.stomp.websocketstompmessageflow; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.stereotype.Controller; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Controller +public class GreetingController { + + @MessageMapping("/greeting") + public String handle(String greeting) { + return "[" + getTimestamp() + ": " + greeting; + } + + private String getTimestamp() { + return new SimpleDateFormat("MM/dd/yyyy h:mm:ss a").format(new Date()); + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/WebSocketConfiguration.java new file mode 100644 index 000000000000..4198cc93e94a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/WebSocketConfiguration.java @@ -0,0 +1,41 @@ +/* + * 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.stomp.websocketstompmessageflow; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/portfolio"); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app"); + registry.enableSimpleBroker("/topic"); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/PublishOrderWebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/PublishOrderWebSocketConfiguration.java new file mode 100644 index 000000000000..527d2bb7bab0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/PublishOrderWebSocketConfiguration.java @@ -0,0 +1,36 @@ +/* + * 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.stomp.websocketstomporderedmessages; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class PublishOrderWebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // ... + registry.setPreservePublishOrder(true); + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/ReceiveOrderWebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/ReceiveOrderWebSocketConfiguration.java new file mode 100644 index 000000000000..9f787007bf64 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/ReceiveOrderWebSocketConfiguration.java @@ -0,0 +1,35 @@ +/* + * 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.stomp.websocketstomporderedmessages; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class ReceiveOrderWebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.setPreserveReceiveOrder(true); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/JettyWebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/JettyWebSocketConfiguration.java new file mode 100644 index 000000000000..78e4c99773e9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/JettyWebSocketConfiguration.java @@ -0,0 +1,49 @@ +/* + * 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.stomp.websocketstompserverconfig; + +import java.time.Duration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy; +import org.springframework.web.socket.server.support.DefaultHandshakeHandler; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class JettyWebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler()); + } + + @Bean + public DefaultHandshakeHandler handshakeHandler() { + JettyRequestUpgradeStrategy strategy = new JettyRequestUpgradeStrategy(); + strategy.addWebSocketConfigurer(configurable -> { + configurable.setInputBufferSize(4 * 8192); + configurable.setIdleTimeout(Duration.ofSeconds(600)); + }); + return new DefaultHandshakeHandler(strategy); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/WebSocketConfiguration.java new file mode 100644 index 000000000000..b43079e82b04 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/WebSocketConfiguration.java @@ -0,0 +1,35 @@ +/* + * 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.stomp.websocketstompserverconfig; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registry) { + registry.setMessageSizeLimit(4 * 8192); + registry.setTimeToFirstMessage(30000); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketfallbacksockjsenable/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketfallbacksockjsenable/WebSocketConfiguration.java new file mode 100644 index 000000000000..dde6939608b6 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketfallbacksockjsenable/WebSocketConfiguration.java @@ -0,0 +1,43 @@ +/* + * 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.websocketfallbacksockjsenable; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.docs.web.websocket.websocketserverhandler.MyHandler; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +// tag::snippet[] +@Configuration +@EnableWebSocket +public class WebSocketConfiguration implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(myHandler(), "/myHandler").withSockJS(); + } + + @Bean + public WebSocketHandler myHandler() { + return new MyHandler(); + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverallowedorigins/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverallowedorigins/WebSocketConfiguration.java new file mode 100644 index 000000000000..632136a0dacf --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverallowedorigins/WebSocketConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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.websocketserverallowedorigins; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.docs.web.websocket.websocketserverhandler.MyHandler; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +// tag::snippet[] +@Configuration +@EnableWebSocket +public class WebSocketConfiguration implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com"); + } + + @Bean + public WebSocketHandler myHandler() { + return new MyHandler(); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverhandler/MyHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverhandler/MyHandler.java new file mode 100644 index 000000000000..ed4be8e7d7f1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverhandler/MyHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.websocket.websocketserverhandler; + +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +// tag::snippet[] +public class MyHandler extends TextWebSocketHandler { + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverhandler/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverhandler/WebSocketConfiguration.java new file mode 100644 index 000000000000..753a598eedaf --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverhandler/WebSocketConfiguration.java @@ -0,0 +1,41 @@ +/* + * 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.websocketserverhandler; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +// tag::snippet[] +@Configuration +@EnableWebSocket +public class WebSocketConfiguration implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(myHandler(), "/myHandler"); + } + + @Bean + public WebSocketHandler myHandler() { + return new MyHandler(); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverhandshake/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverhandshake/WebSocketConfiguration.java new file mode 100644 index 000000000000..87b5d64325f5 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverhandshake/WebSocketConfiguration.java @@ -0,0 +1,38 @@ +/* + * 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.websocketserverhandshake; + +import org.springframework.context.annotation.Configuration; +import org.springframework.docs.web.websocket.websocketserverhandler.MyHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; + +// tag::snippet[] +@Configuration +@EnableWebSocket +public class WebSocketConfiguration implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(new MyHandler(), "/myHandler") + .addInterceptors(new HttpSessionHandshakeInterceptor()); + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/JettyWebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/JettyWebSocketConfiguration.java new file mode 100644 index 000000000000..07897a2a2d31 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/JettyWebSocketConfiguration.java @@ -0,0 +1,55 @@ +/* + * 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 java.time.Duration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy; +import org.springframework.web.socket.server.support.DefaultHandshakeHandler; + +// tag::snippet[] +@Configuration +@EnableWebSocket +public class JettyWebSocketConfiguration implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(echoWebSocketHandler(), "/echo").setHandshakeHandler(handshakeHandler()); + } + + @Bean + public WebSocketHandler echoWebSocketHandler() { + return new MyEchoHandler(); + } + + @Bean + public DefaultHandshakeHandler handshakeHandler() { + JettyRequestUpgradeStrategy strategy = new JettyRequestUpgradeStrategy(); + strategy.addWebSocketConfigurer(configurable -> { + configurable.setInputBufferSize(8192); + configurable.setIdleTimeout(Duration.ofSeconds(600)); + }); + return new DefaultHandshakeHandler(strategy); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/MyEchoHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/MyEchoHandler.java new file mode 100644 index 000000000000..76f78d5e9785 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/MyEchoHandler.java @@ -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; + +public class MyEchoHandler extends AbstractWebSocketHandler { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/WebSocketConfiguration.java new file mode 100644 index 000000000000..95c74290c5c0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/WebSocketConfiguration.java @@ -0,0 +1,35 @@ +/* + * 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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; + +// tag::snippet[] +@Configuration +public class WebSocketConfiguration { + + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); + container.setMaxTextMessageBufferSize(8192); + container.setMaxBinaryMessageBufferSize(8192); + return container; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/aopajltwspring/ApplicationConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/aopajltwspring/ApplicationConfiguration.kt new file mode 100644 index 000000000000..8c5635a64111 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/aopajltwspring/ApplicationConfiguration.kt @@ -0,0 +1,26 @@ +/* + * 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.aop.aopajltwspring + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableLoadTimeWeaving + +// tag::snippet[] +@Configuration +@EnableLoadTimeWeaving +class ApplicationConfiguration +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/aopajltwspring/CustomWeaverConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/aopajltwspring/CustomWeaverConfiguration.kt new file mode 100644 index 000000000000..587efae332a1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/aopajltwspring/CustomWeaverConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.aop.aopajltwspring + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableLoadTimeWeaving +import org.springframework.context.annotation.LoadTimeWeavingConfigurer +import org.springframework.instrument.classloading.LoadTimeWeaver +import org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver + +// tag::snippet[] +@Configuration +@EnableLoadTimeWeaving +class CustomWeaverConfiguration : LoadTimeWeavingConfigurer { + + override fun getLoadTimeWeaver(): LoadTimeWeaver { + return ReflectiveLoadTimeWeaver() + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/aopatconfigurable/ApplicationConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/aopatconfigurable/ApplicationConfiguration.kt new file mode 100644 index 000000000000..ff92f4e372ef --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/aopatconfigurable/ApplicationConfiguration.kt @@ -0,0 +1,26 @@ +/* + * 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.aop.aopatconfigurable + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.aspectj.EnableSpringConfigured + +// tag::snippet[] +@Configuration +@EnableSpringConfigured +class ApplicationConfiguration +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopaspectjsupport/ApplicationConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopaspectjsupport/ApplicationConfiguration.kt new file mode 100644 index 000000000000..ca3544a39920 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopaspectjsupport/ApplicationConfiguration.kt @@ -0,0 +1,26 @@ +/* + * 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.aop.ataspectj.aopaspectjsupport + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableAspectJAutoProxy + +// tag::snippet[] +@Configuration +@EnableAspectJAutoProxy +class ApplicationConfiguration +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectj/ApplicationConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectj/ApplicationConfiguration.kt new file mode 100644 index 000000000000..ebeb56710f92 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectj/ApplicationConfiguration.kt @@ -0,0 +1,29 @@ +/* + * 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.aop.ataspectj.aopataspectj + +import org.springframework.context.annotation.Bean + +// tag::snippet[] +class ApplicationConfiguration { + + @Bean + fun myAspect() = NotVeryUsefulAspect().apply { + // Configure properties of the aspect here + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectj/NotVeryUsefulAspect.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectj/NotVeryUsefulAspect.kt new file mode 100644 index 000000000000..53942988ecbd --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectj/NotVeryUsefulAspect.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.core.aop.ataspectj.aopataspectj + +import org.aspectj.lang.annotation.Aspect + +// tag::snippet[] +@Aspect +class NotVeryUsefulAspect +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ApplicationConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ApplicationConfiguration.kt new file mode 100644 index 000000000000..3e961535846c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ApplicationConfiguration.kt @@ -0,0 +1,33 @@ +/* + * 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.aop.ataspectj.aopataspectjexample + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableAspectJAutoProxy + +// tag::snippet[] +@Configuration +@EnableAspectJAutoProxy +class ApplicationConfiguration { + + @Bean + fun concurrentOperationExecutor() = ConcurrentOperationExecutor().apply { + maxRetries = 3 + order = 100 + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ConcurrentOperationExecutor.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ConcurrentOperationExecutor.kt new file mode 100644 index 000000000000..c4f918f51fec --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ConcurrentOperationExecutor.kt @@ -0,0 +1,59 @@ +/* + * 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.aop.ataspectj.aopataspectjexample + +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.springframework.core.Ordered +import org.springframework.dao.PessimisticLockingFailureException + +// tag::snippet[] +@Aspect +class ConcurrentOperationExecutor : Ordered { + + companion object { + private const val DEFAULT_MAX_RETRIES = 2 + } + + var maxRetries = DEFAULT_MAX_RETRIES + + private var order = 1 + + override fun getOrder(): Int { + return this.order + } + + fun setOrder(order: Int) { + this.order = order + } + + @Around("com.xyz.CommonPointcuts.businessService()") + fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any { + var numAttempts = 0 + var lockFailureException: PessimisticLockingFailureException? + do { + numAttempts++ + try { + return pjp.proceed() + } catch (ex: PessimisticLockingFailureException) { + lockFailureException = ex + } + } while (numAttempts <= this.maxRetries) + throw lockFailureException!! + } +} // end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/Idempotent.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/Idempotent.kt new file mode 100644 index 000000000000..89224a8d4884 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/Idempotent.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.core.aop.ataspectj.aopataspectjexample.service + +// tag::snippet[] +@Retention(AnnotationRetention.RUNTIME) +// marker annotation +annotation class Idempotent +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/SampleService.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/SampleService.kt new file mode 100644 index 000000000000..da5131d74869 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/service/SampleService.kt @@ -0,0 +1,35 @@ +/* + * 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.aop.ataspectj.aopataspectjexample.service + +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.springframework.stereotype.Service + +@Service +class SampleService { + + // tag::snippet[] + @Around("execution(* com.xyz..service.*.*(..)) && " + + "@annotation(com.xyz.service.Idempotent)") + fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? { + // ... + return pjp.proceed(pjp.args) + } + // end::snippet[] + +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aopapi/aopapipointcutsregex/JdkRegexpConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aopapi/aopapipointcutsregex/JdkRegexpConfiguration.kt new file mode 100644 index 000000000000..16a6cba610bb --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aopapi/aopapipointcutsregex/JdkRegexpConfiguration.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.core.aopapi.aopapipointcutsregex + +import org.springframework.aop.support.JdkRegexpMethodPointcut +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +// tag::snippet[] +@Configuration +class JdkRegexpConfiguration { + + @Bean + fun settersAndAbsquatulatePointcut() = JdkRegexpMethodPointcut().apply { + setPatterns(".*set.*", ".*absquatulate") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/aopapi/aopapipointcutsregex/RegexpConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/aopapi/aopapipointcutsregex/RegexpConfiguration.kt new file mode 100644 index 000000000000..2ae08344f64f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/aopapi/aopapipointcutsregex/RegexpConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.aopapi.aopapipointcutsregex + +import org.aopalliance.aop.Advice +import org.springframework.aop.support.RegexpMethodPointcutAdvisor +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +// tag::snippet[] +@Configuration +class RegexpConfiguration { + + @Bean + fun settersAndAbsquatulateAdvisor(beanNameOfAopAllianceInterceptor: Advice) = RegexpMethodPointcutAdvisor().apply { + advice = beanNameOfAopAllianceInterceptor + setPatterns(".*set.*", ".*absquatulate") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.kt new file mode 100644 index 000000000000..322698b06fa1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.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.beans.dependencies.beansfactorylazyinit + +class AnotherBean { +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ApplicationConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ApplicationConfiguration.kt new file mode 100644 index 000000000000..cf19c8859a7e --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ApplicationConfiguration.kt @@ -0,0 +1,38 @@ +/* + * 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.beans.dependencies.beansfactorylazyinit + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Lazy + +@Configuration +class ApplicationConfiguration { + + // tag::snippet[] + @Bean + @Lazy + fun lazy(): ExpensiveToCreateBean { + return ExpensiveToCreateBean() + } + + @Bean + fun notLazy(): AnotherBean { + return AnotherBean() + } + // end::snippet[] +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.kt new file mode 100644 index 000000000000..b70d3a922512 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.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.beans.dependencies.beansfactorylazyinit + +class ExpensiveToCreateBean { +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/LazyConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/LazyConfiguration.kt new file mode 100644 index 000000000000..0cd083e0fdb1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/LazyConfiguration.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.core.beans.dependencies.beansfactorylazyinit + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Lazy + +// tag::snippet[] +@Configuration +@Lazy +class LazyConfiguration { + // No bean will be pre-instantiated... +} +// end::snippet[] \ 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/FieldValueTestBean.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/FieldValueTestBean.kt new file mode 100644 index 000000000000..4130074c621f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/FieldValueTestBean.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.core.expressions.expressionsbeandef + +import org.springframework.beans.factory.annotation.Value + +// tag::snippet[] +class FieldValueTestBean { + + @field:Value("#{ systemProperties['user.region'] }") + lateinit var defaultLocale: String +} +// end::snippet[] 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/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/MovieRecommender.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/MovieRecommender.kt new file mode 100644 index 000000000000..6dd298a7bc7a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/MovieRecommender.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.core.expressions.expressionsbeandef + +import org.springframework.beans.factory.annotation.Value + +// tag::snippet[] +class MovieRecommender(private val customerPreferenceDao: CustomerPreferenceDao, + @Value("#{systemProperties['user.country']}") + private val defaultLocale: String) { + // ... +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/NumberGuess.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/NumberGuess.kt new file mode 100644 index 000000000000..e6dc38a729aa --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/NumberGuess.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.core.expressions.expressionsbeandef + +import org.springframework.beans.factory.annotation.Value + +// tag::snippet[] +class NumberGuess { + + @set:Value("#{ T(java.lang.Math).random() * 100.0 }") + var randomNumber: Double = 0.0 +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/PropertyValueTestBean.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/PropertyValueTestBean.kt new file mode 100644 index 000000000000..ee79955657da --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/PropertyValueTestBean.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.core.expressions.expressionsbeandef + +import org.springframework.beans.factory.annotation.Value + +// tag::snippet[] +class PropertyValueTestBean { + + @set:Value("#{ systemProperties['user.region'] }") + lateinit var defaultLocale: String +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/ShapeGuess.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/ShapeGuess.kt new file mode 100644 index 000000000000..ee713a26d5af --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/ShapeGuess.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.core.expressions.expressionsbeandef + +import org.springframework.beans.factory.annotation.Value + +// tag::snippet[] +class ShapeGuess { + + @set:Value("#{ numberGuess.randomNumber }") + var initialShapeSeed: Double = 0.0 +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/SimpleMovieLister.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/SimpleMovieLister.kt new file mode 100644 index 000000000000..f863b880c75f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/SimpleMovieLister.kt @@ -0,0 +1,37 @@ +/* + * 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 + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value + +// tag::snippet[] +class SimpleMovieLister { + + private lateinit var movieFinder: MovieFinder + private lateinit var defaultLocale: String + + @Autowired + fun configure(movieFinder: MovieFinder, + @Value("#{ systemProperties['user.region'] }") defaultLocale: String) { + this.movieFinder = movieFinder + this.defaultLocale = defaultLocale + } + + // ... +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/formatconfiguringformattingglobaldatetimeformat/ApplicationConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/formatconfiguringformattingglobaldatetimeformat/ApplicationConfiguration.kt new file mode 100644 index 000000000000..c4688db94871 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/formatconfiguringformattingglobaldatetimeformat/ApplicationConfiguration.kt @@ -0,0 +1,52 @@ +/* + * 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.docs.core.validation.formatconfiguringformattingglobaldatetimeformat + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.format.datetime.DateFormatter +import org.springframework.format.datetime.DateFormatterRegistrar +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar +import org.springframework.format.number.NumberFormatAnnotationFormatterFactory +import org.springframework.format.support.DefaultFormattingConversionService +import org.springframework.format.support.FormattingConversionService +import java.time.format.DateTimeFormatter + +// tag::snippet[] +@Configuration +class ApplicationConfiguration { + + @Bean + fun conversionService(): FormattingConversionService { + // Use the DefaultFormattingConversionService but do not register defaults + return DefaultFormattingConversionService(false).apply { + + // Ensure @NumberFormat is still supported + addFormatterForFieldAnnotation(NumberFormatAnnotationFormatterFactory()) + + // Register JSR-310 date conversion with a specific global format + val dateTimeRegistrar = DateTimeFormatterRegistrar() + dateTimeRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")) + dateTimeRegistrar.registerFormatters(this) + + // Register date conversion with a specific global format + val dateRegistrar = DateFormatterRegistrar() + dateRegistrar.setFormatter(DateFormatter("yyyyMMdd")) + dateRegistrar.registerFormatters(this) + } + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.kt new file mode 100644 index 000000000000..be0946c8376a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.validation.validationbeanvalidationspringmethod + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor + +// tag::snippet[] +@Configuration +class ApplicationConfiguration { + + companion object { + + @Bean + @JvmStatic + fun validationPostProcessor() = MethodValidationPostProcessor() + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.kt new file mode 100644 index 000000000000..07e383481edb --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.kt @@ -0,0 +1,36 @@ +/* + * 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.validation.validationbeanvalidationspringmethodexceptions + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor + +// tag::snippet[] +@Configuration +class ApplicationConfiguration { + + companion object { + + @Bean + @JvmStatic + fun validationPostProcessor() = MethodValidationPostProcessor().apply { + setAdaptConstraintViolations(true) + } + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.kt new file mode 100644 index 000000000000..808f57895fe8 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.kt @@ -0,0 +1,57 @@ +/* + * 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.jdbccomplextypes + +import oracle.jdbc.driver.OracleConnection +import org.springframework.jdbc.core.SqlTypeValue +import org.springframework.jdbc.core.support.AbstractSqlTypeValue +import java.sql.Connection +import java.sql.Date +import java.text.SimpleDateFormat + +@Suppress("unused") +class SqlTypeValueFactory { + + fun createStructSample(): AbstractSqlTypeValue { + // tag::struct[] + val testItem = TestItem(123L, "A test item", + SimpleDateFormat("yyyy-M-d").parse("2010-12-31")) + + val value = object : AbstractSqlTypeValue() { + override fun createTypeValue(connection: Connection, sqlType: Int, typeName: String?): Any { + val item = arrayOf(testItem.id, testItem.description, + Date(testItem.expirationDate.time)) + return connection.createStruct(typeName, item) + } + } + // end::struct[] + return value + } + + fun createOracleArray() : SqlTypeValue { + // tag::oracle-array[] + val ids = arrayOf(1L, 2L) + val value: SqlTypeValue = object : AbstractSqlTypeValue() { + override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { + return conn.unwrap(OracleConnection::class.java).createOracleArray(typeName, ids) + } + } + // end::oracle-array[] + return value + } + +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.kt new file mode 100644 index 000000000000..ef93410bb2b1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.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.dataaccess.jdbc.jdbccomplextypes + +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 new file mode 100644 index 000000000000..5cdfb06bf389 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.kt @@ -0,0 +1,42 @@ +/* + * 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.jdbccomplextypes + +import org.springframework.jdbc.core.SqlOutParameter +import org.springframework.jdbc.`object`.StoredProcedure +import java.sql.CallableStatement +import java.sql.Struct +import java.sql.Types +import java.util.Date +import javax.sql.DataSource + +@Suppress("unused") +class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, "get_item") { + init { + declareParameter(SqlOutParameter("item",Types.STRUCT,"ITEM_TYPE") { + cs: CallableStatement, colIndx: Int, _: Int, _: String? -> + val struct = cs.getObject(colIndx) as Struct + val attr = struct.attributes + TestItem( + (attr[0] as Number).toLong(), + attr[1] as String, + attr[2] as Date + ) + }) + // ... + } +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt new file mode 100644 index 000000000000..1f29d0e8fef7 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt @@ -0,0 +1,19 @@ +package org.springframework.docs.dataaccess.jdbc.jdbcdatasource + +import org.apache.commons.dbcp2.BasicDataSource +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class BasicDataSourceConfiguration { + + // tag::snippet[] + @Bean(destroyMethod = "close") + fun dataSource() = BasicDataSource().apply { + driverClassName = "org.hsqldb.jdbcDriver" + url = "jdbc:hsqldb:hsql://localhost:" + username = "sa" + password = "" + } + // end::snippet[] +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt new file mode 100644 index 000000000000..0c290c9ebd5f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt @@ -0,0 +1,20 @@ +package org.springframework.docs.dataaccess.jdbc.jdbcdatasource + +import com.mchange.v2.c3p0.ComboPooledDataSource +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +internal class ComboPooledDataSourceConfiguration { + + // tag::snippet[] + @Bean(destroyMethod = "close") + fun dataSource() = ComboPooledDataSource().apply { + driverClass = "org.hsqldb.jdbcDriver" + jdbcUrl = "jdbc:hsqldb:hsql://localhost:" + user = "sa" + password = "" + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt new file mode 100644 index 000000000000..87692df00b05 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt @@ -0,0 +1,36 @@ +/* + * 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.docs.dataaccess.jdbc.jdbcdatasource + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.DriverManagerDataSource + +@Configuration +class DriverManagerDataSourceConfiguration { + + // tag::snippet[] + @Bean + fun dataSource() = DriverManagerDataSource().apply { + setDriverClassName("org.hsqldb.jdbcDriver") + url = "jdbc:hsqldb:hsql://localhost:" + username = "sa" + password = "" + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.kt new file mode 100644 index 000000000000..7eab9f733af1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.kt @@ -0,0 +1,36 @@ +/* + * 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.jdbcembeddeddatabase + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType + +@Configuration +class JdbcEmbeddedDatabaseConfiguration { + + // tag::snippet[] + @Bean + fun dataSource() = EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(EmbeddedDatabaseType.H2) + .addScripts("schema.sql", "test-data.sql") + .build() + // end::snippet[] + +} 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/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDao.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDao.kt new file mode 100644 index 000000000000..0591b7c2765a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDao.kt @@ -0,0 +1,28 @@ +/* + * 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 + +import org.springframework.jdbc.core.JdbcTemplate +import javax.sql.DataSource + +// tag::snippet[] +class JdbcCorporateEventDao(dataSource: DataSource): CorporateEventDao { + + private val jdbcTemplate = JdbcTemplate(dataSource) + + // JDBC-backed implementations of the methods on the CorporateEventDao follow... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDaoConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDaoConfiguration.kt new file mode 100644 index 000000000000..ceb82cc7e0c7 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDaoConfiguration.kt @@ -0,0 +1,40 @@ +/* + * 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 + +import org.apache.commons.dbcp2.BasicDataSource +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import javax.sql.DataSource + +@Configuration +class JdbcCorporateEventDaoConfiguration { + + // tag::snippet[] + @Bean + fun corporateEventDao(dataSource: DataSource) = JdbcCorporateEventDao(dataSource) + + @Bean(destroyMethod = "close") + fun dataSource() = BasicDataSource().apply { + driverClassName = "org.hsqldb.jdbcDriver" + url = "jdbc:hsqldb:hsql://localhost:" + username = "sa" + password = "" + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepositoryConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepositoryConfiguration.kt new file mode 100644 index 000000000000..0c3e8c11a079 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepositoryConfiguration.kt @@ -0,0 +1,38 @@ +/* + * 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 + +import org.apache.commons.dbcp2.BasicDataSource +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + +// tag::snippet[] +@Configuration +@ComponentScan("org.example.jdbc") +class JdbcCorporateEventRepositoryConfiguration { + + @Bean(destroyMethod = "close") + fun dataSource() = BasicDataSource().apply { + driverClassName = "org.hsqldb.jdbcDriver" + url = "jdbc:hsqldb:hsql://localhost:" + username = "sa" + password = "" + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cacheannotationenable/CacheConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cacheannotationenable/CacheConfiguration.kt new file mode 100644 index 000000000000..674b8aad85c6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cacheannotationenable/CacheConfiguration.kt @@ -0,0 +1,37 @@ +/* + * 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.cache.cacheannotationenable + +import org.springframework.cache.CacheManager +import org.springframework.cache.annotation.EnableCaching +import org.springframework.cache.caffeine.CaffeineCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +// tag::snippet[] +@Configuration +@EnableCaching +class CacheConfiguration { + + @Bean + fun cacheManager(): CacheManager { + return CaffeineCacheManager().apply { + setCacheSpecification("...") + } + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CacheConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CacheConfiguration.kt new file mode 100644 index 000000000000..8e85c14bbbe9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CacheConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.cache.cachestoreconfigurationcaffeine + +import org.springframework.cache.CacheManager +import org.springframework.cache.caffeine.CaffeineCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@Configuration +class CacheConfiguration { + + // tag::snippet[] + @Bean + fun cacheManager(): CacheManager { + return CaffeineCacheManager() + } + // end::snippet[] +} \ 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 new file mode 100644 index 000000000000..0db14f210f45 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.cache.cachestoreconfigurationcaffeine + +import org.springframework.cache.CacheManager +import org.springframework.cache.caffeine.CaffeineCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@Configuration +class CustomCacheConfiguration { + + // tag::snippet[] + @Bean + fun cacheManager(): CacheManager { + return CaffeineCacheManager("default", "books") + } + // end::snippet[] +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationjdk/CacheConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationjdk/CacheConfiguration.kt new file mode 100644 index 000000000000..779b1168eb95 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationjdk/CacheConfiguration.kt @@ -0,0 +1,52 @@ +/* + * 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.cache.cachestoreconfigurationjdk + +import org.springframework.cache.CacheManager +import org.springframework.cache.concurrent.ConcurrentMapCache +import org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean +import org.springframework.cache.support.SimpleCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@Configuration +class CacheConfiguration { + + // tag::snippet[] + @Bean + fun defaultCache(): ConcurrentMapCacheFactoryBean { + return ConcurrentMapCacheFactoryBean().apply { + setName("default") + } + } + + @Bean + fun booksCache(): ConcurrentMapCacheFactoryBean { + return ConcurrentMapCacheFactoryBean().apply { + setName("books") + } + } + + @Bean + fun cacheManager(defaultCache: ConcurrentMapCache, booksCache: ConcurrentMapCache): CacheManager { + return SimpleCacheManager().apply { + setCaches(setOf(defaultCache, booksCache)) + } + } + // end::snippet[] +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationjsr107/CacheConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationjsr107/CacheConfiguration.kt new file mode 100644 index 000000000000..308f60cd7cfd --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationjsr107/CacheConfiguration.kt @@ -0,0 +1,39 @@ +/* + * 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.cache.cachestoreconfigurationjsr107 + +import org.springframework.cache.jcache.JCacheCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import javax.cache.Caching + +@Suppress("UsePropertyAccessSyntax") +@Configuration +class CacheConfiguration { + + // tag::snippet[] + @Bean + fun jCacheManager(): javax.cache.CacheManager { + val cachingProvider = Caching.getCachingProvider() + return cachingProvider.getCacheManager() + } + + @Bean + fun cacheManager(jCacheManager: javax.cache.CacheManager): org.springframework.cache.CacheManager { + return JCacheCacheManager(jCacheManager) + } + // end::snippet[] +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationnoop/CacheConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationnoop/CacheConfiguration.kt new file mode 100644 index 000000000000..5fc16ea91edc --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationnoop/CacheConfiguration.kt @@ -0,0 +1,43 @@ +/* + * 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.cache.cachestoreconfigurationnoop + +import org.springframework.cache.CacheManager +import org.springframework.cache.support.CompositeCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class CacheConfiguration { + + private fun jdkCache(): CacheManager? { + return null + } + + private fun gemfireCache(): CacheManager? { + return null + } + + // tag::snippet[] + @Bean + fun cacheManager(jdkCache: CacheManager, gemfireCache: CacheManager): CacheManager { + return CompositeCacheManager().apply { + setCacheManagers(listOf(jdkCache, gemfireCache)) + setFallbackToNoOpCache(true) + } + } + // end::snippet[] +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsannotatedsupport/JmsConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsannotatedsupport/JmsConfiguration.kt new file mode 100644 index 000000000000..2972d22898d6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsannotatedsupport/JmsConfiguration.kt @@ -0,0 +1,40 @@ +/* + * 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.jms.jmsannotatedsupport + +import jakarta.jms.ConnectionFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jms.annotation.EnableJms +import org.springframework.jms.config.DefaultJmsListenerContainerFactory +import org.springframework.jms.support.destination.DestinationResolver + +// tag::snippet[] +@Configuration +@EnableJms +class JmsConfiguration { + + @Bean + fun jmsListenerContainerFactory(connectionFactory: ConnectionFactory, destinationResolver: DestinationResolver) = + DefaultJmsListenerContainerFactory().apply { + setConnectionFactory(connectionFactory) + setDestinationResolver(destinationResolver) + setSessionTransacted(true) + setConcurrency("3-10") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/AlternativeJmsConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/AlternativeJmsConfiguration.kt new file mode 100644 index 000000000000..9127de9c7a47 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/AlternativeJmsConfiguration.kt @@ -0,0 +1,41 @@ +/* + * 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.jms.jmsjcamessageendpointmanager + +import jakarta.jms.MessageListener +import jakarta.resource.spi.ResourceAdapter +import org.apache.activemq.ra.ActiveMQActivationSpec +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager + +@Configuration +class AlternativeJmsConfiguration { + + // tag::snippet[] + @Bean + fun jmsMessageEndpointManager( + resourceAdapter: ResourceAdapter, myMessageListener: MessageListener) = JmsMessageEndpointManager().apply { + setResourceAdapter(resourceAdapter) + activationSpec = ActiveMQActivationSpec().apply { + destination = "myQueue" + destinationType = "jakarta.jms.Queue" + } + messageListener = myMessageListener + } + // end::snippet[] +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/JmsConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/JmsConfiguration.kt new file mode 100644 index 000000000000..d7fecece981b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/JmsConfiguration.kt @@ -0,0 +1,40 @@ +/* + * 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.jms.jmsjcamessageendpointmanager + +import jakarta.jms.MessageListener +import jakarta.resource.spi.ResourceAdapter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jms.listener.endpoint.JmsActivationSpecConfig +import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager + +@Configuration +class JmsConfiguration { + + // tag::snippet[] + @Bean + fun jmsMessageEndpointManager( + resourceAdapter: ResourceAdapter, myMessageListener: MessageListener) = JmsMessageEndpointManager().apply { + setResourceAdapter(resourceAdapter) + activationSpecConfig = JmsActivationSpecConfig().apply { + destinationName = "myQueue" + } + messageListener = myMessageListener + } + // end::snippet[] +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasync/ExampleListener.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasync/ExampleListener.kt new file mode 100644 index 000000000000..002813ae7b4c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasync/ExampleListener.kt @@ -0,0 +1,39 @@ +/* + * 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.jms.jmsreceivingasync + +import jakarta.jms.JMSException +import jakarta.jms.Message +import jakarta.jms.MessageListener +import jakarta.jms.TextMessage + +// tag::snippet[] +class ExampleListener : MessageListener { + + override fun onMessage(message: Message) { + if (message is TextMessage) { + try { + println(message.text) + } catch (ex: JMSException) { + throw RuntimeException(ex) + } + } else { + throw IllegalArgumentException("Message must be of type TextMessage") + } + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasync/JmsConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasync/JmsConfiguration.kt new file mode 100644 index 000000000000..2b0975e2778c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasync/JmsConfiguration.kt @@ -0,0 +1,40 @@ +/* + * 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.jms.jmsreceivingasync + +import jakarta.jms.ConnectionFactory +import jakarta.jms.Destination +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jms.listener.DefaultMessageListenerContainer + +@Configuration +class JmsConfiguration { + + // tag::snippet[] + @Bean + fun messageListener() = ExampleListener() + + @Bean + fun jmsContainer(connectionFactory: ConnectionFactory, destination: Destination, messageListener: ExampleListener) = + DefaultMessageListenerContainer().apply { + setConnectionFactory(connectionFactory) + setDestination(destination) + setMessageListener(messageListener) + } + // end::snippet[] +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultMessageDelegate.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultMessageDelegate.kt new file mode 100644 index 000000000000..c28c9a1ec6c0 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultMessageDelegate.kt @@ -0,0 +1,40 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter + +import java.io.Serializable + +// tag::snippet[] +class DefaultMessageDelegate : MessageDelegate { + + override fun handleMessage(message: String) { + // ... + } + + override fun handleMessage(message: Map<*, *>) { + // ... + } + + override fun handleMessage(message: ByteArray) { + // ... + } + + override fun handleMessage(message: Serializable) { + // ... + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultResponsiveTextMessageDelegate.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultResponsiveTextMessageDelegate.kt new file mode 100644 index 000000000000..fd4c58b6da22 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultResponsiveTextMessageDelegate.kt @@ -0,0 +1,28 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter + +import jakarta.jms.TextMessage + +// tag::snippet[] +class DefaultResponsiveTextMessageDelegate : ResponsiveTextMessageDelegate { + + override fun receive(message: TextMessage): String { + return "message" + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultTextMessageDelegate.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultTextMessageDelegate.kt new file mode 100644 index 000000000000..3a4ab53c5e2c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/DefaultTextMessageDelegate.kt @@ -0,0 +1,28 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter + +import org.springframework.web.socket.TextMessage + +// tag::snippet[] +class DefaultTextMessageDelegate : TextMessageDelegate { + + override fun receive(message: TextMessage) { + // ... + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/JmsConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/JmsConfiguration.kt new file mode 100644 index 000000000000..14ff4c8c515b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/JmsConfiguration.kt @@ -0,0 +1,44 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter + +import jakarta.jms.ConnectionFactory +import jakarta.jms.Destination +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.docs.integration.jms.jmsreceivingasync.ExampleListener +import org.springframework.jms.listener.DefaultMessageListenerContainer +import org.springframework.jms.listener.adapter.MessageListenerAdapter + +@Configuration +class JmsConfiguration { + + // tag::snippet[] + @Bean + fun messageListener(messageDelegate: DefaultMessageDelegate): MessageListenerAdapter { + return MessageListenerAdapter(messageDelegate) + } + + @Bean + fun jmsContainer(connectionFactory: ConnectionFactory, destination: Destination, messageListener: ExampleListener) = + DefaultMessageListenerContainer().apply { + setConnectionFactory(connectionFactory) + setDestination(destination) + setMessageListener(messageListener) + } + // end::snippet[] +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageDelegate.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageDelegate.kt new file mode 100644 index 000000000000..fe55a8e740d0 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageDelegate.kt @@ -0,0 +1,28 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter + +import java.io.Serializable + +// tag::snippet[] +interface MessageDelegate { + fun handleMessage(message: String) + fun handleMessage(message: Map<*, *>) + fun handleMessage(message: ByteArray) + fun handleMessage(message: Serializable) +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageListenerConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageListenerConfiguration.kt new file mode 100644 index 000000000000..b77d0dc85612 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageListenerConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.jms.jmsreceivingasyncmessagelisteneradapter + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jms.listener.adapter.MessageListenerAdapter + +@Configuration +class MessageListenerConfiguration { + + // tag::snippet[] + @Bean + fun messageListener(messageDelegate: DefaultTextMessageDelegate) = MessageListenerAdapter(messageDelegate).apply { + setDefaultListenerMethod("receive") + // We don't want automatic message context extraction + setMessageConverter(null) + } + // end::snippet[] +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/ResponsiveTextMessageDelegate.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/ResponsiveTextMessageDelegate.kt new file mode 100644 index 000000000000..7f6ba5eacc3f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/ResponsiveTextMessageDelegate.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.integration.jms.jmsreceivingasyncmessagelisteneradapter + +import jakarta.jms.TextMessage + +// tag::snippet[] +interface ResponsiveTextMessageDelegate { + + // Notice the return type... + fun receive(message: TextMessage): String +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/TextMessageDelegate.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/TextMessageDelegate.kt new file mode 100644 index 000000000000..d90a04b9b3ef --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/TextMessageDelegate.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.integration.jms.jmsreceivingasyncmessagelisteneradapter + +import org.springframework.web.socket.TextMessage + +// tag::snippet[] +interface TextMessageDelegate { + fun receive(message: TextMessage) +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmstxparticipation/ExternalTxJmsConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmstxparticipation/ExternalTxJmsConfiguration.kt new file mode 100644 index 000000000000..1805e2c373b9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmstxparticipation/ExternalTxJmsConfiguration.kt @@ -0,0 +1,46 @@ +/* + * 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.jms.jmstxparticipation + +import jakarta.jms.ConnectionFactory +import jakarta.jms.Destination +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.docs.integration.jms.jmsreceivingasync.ExampleListener +import org.springframework.jms.listener.DefaultMessageListenerContainer +import org.springframework.transaction.jta.JtaTransactionManager + +@Configuration +class ExternalTxJmsConfiguration { + + // tag::transactionManagerSnippet[] + @Bean + fun transactionManager() = JtaTransactionManager() + // end::transactionManagerSnippet[] + + // tag::jmsContainerSnippet[] + @Bean + fun jmsContainer(connectionFactory: ConnectionFactory, destination: Destination, messageListener: ExampleListener, + transactionManager: JtaTransactionManager) = + DefaultMessageListenerContainer().apply { + setConnectionFactory(connectionFactory) + setDestination(destination) + setMessageListener(messageListener) + setTransactionManager(transactionManager) + } + // end::jmsContainerSnippet[] +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmstxparticipation/JmsConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmstxparticipation/JmsConfiguration.kt new file mode 100644 index 000000000000..ce0a5b085cbf --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jms/jmstxparticipation/JmsConfiguration.kt @@ -0,0 +1,39 @@ +/* + * 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.jms.jmstxparticipation + +import jakarta.jms.ConnectionFactory +import jakarta.jms.Destination +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.docs.integration.jms.jmsreceivingasync.ExampleListener +import org.springframework.jms.listener.DefaultMessageListenerContainer + +@Configuration +class JmsConfiguration { + + // tag::snippet[] + @Bean + fun jmsContainer(connectionFactory: ConnectionFactory, destination: Destination, messageListener: ExampleListener) = + DefaultMessageListenerContainer().apply { + setConnectionFactory(connectionFactory) + setDestination(destination) + setMessageListener(messageListener) + isSessionTransacted = true + } + // end::snippet[] +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/CustomJmxConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/CustomJmxConfiguration.kt new file mode 100644 index 000000000000..98d9e5f04551 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/CustomJmxConfiguration.kt @@ -0,0 +1,26 @@ +/* + * 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.jmx.jmxcontextmbeanexport + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableMBeanExport + +// tag::snippet[] +@Configuration +@EnableMBeanExport(server="myMBeanServer", defaultDomain="myDomain") +class CustomJmxConfiguration +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/JmxConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/JmxConfiguration.kt new file mode 100644 index 000000000000..9f88e70ced10 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/JmxConfiguration.kt @@ -0,0 +1,26 @@ +/* + * 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.jmx.jmxcontextmbeanexport + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableMBeanExport + +// tag::snippet[] +@Configuration +@EnableMBeanExport +class JmxConfiguration +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.kt new file mode 100644 index 000000000000..b82b4c1d9c74 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.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.integration.jmx.jmxexporting + +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/JmxConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxConfiguration.kt new file mode 100644 index 000000000000..1f3210110e45 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxConfiguration.kt @@ -0,0 +1,38 @@ +/* + * 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.jmx.jmxexporting + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jmx.export.MBeanExporter + +// tag::snippet[] +@Configuration +class JmxConfiguration { + + @Bean + fun exporter(testBean: JmxTestBean) = MBeanExporter().apply { + setBeans(mapOf("bean:name=testBean1" to testBean)) + } + + @Bean + fun testBean() = JmxTestBean().apply { + name = "TEST" + age = 100 + } +} +// end::snippet[] \ 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 new file mode 100644 index 000000000000..07ae48ed5c69 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.kt @@ -0,0 +1,33 @@ +/* + * 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.jmx.jmxexporting + +// tag::snippet[] +class JmxTestBean : IJmxTestBean { + + override lateinit var name: String + override var age = 0 + + override fun add(x: Int, y: Int): Int { + return x + y + } + + override fun dontExposeMe() { + throw RuntimeException() + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusage/OrderManager.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusage/OrderManager.kt new file mode 100644 index 000000000000..08f7b80d12d4 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusage/OrderManager.kt @@ -0,0 +1,26 @@ +/* + * 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.mailusage + +import org.springframework.docs.integration.mailusagesimple.Order + +// tag::snippet[] +interface OrderManager { + + fun placeOrder(order: Order) +} +// end::snippet[] \ No newline at end of file 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/MailConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/MailConfiguration.kt new file mode 100644 index 000000000000..a4f0378da92c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/MailConfiguration.kt @@ -0,0 +1,49 @@ +/* + * 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 + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mail.SimpleMailMessage +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.mail.javamail.JavaMailSenderImpl + +@Configuration +class MailConfiguration { + + // tag::snippet[] + @Bean + fun mailSender(): JavaMailSender { + return JavaMailSenderImpl().apply { + host = "mail.mycompany.example" + } + } + + @Bean // this is a template message that we can pre-load with default state + fun templateMessage() = SimpleMailMessage().apply { + from = "customerservice@mycompany.example" + subject = "Your order" + } + + + @Bean + fun orderManager(javaMailSender: JavaMailSender, simpleTemplateMessage: SimpleMailMessage) = SimpleOrderManager().apply { + mailSender = javaMailSender + templateMessage = simpleTemplateMessage + } + // end::snippet[] +} 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/integration/mailusagesimple/SimpleOrderManager.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/SimpleOrderManager.kt new file mode 100644 index 000000000000..7be7aa437df3 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/SimpleOrderManager.kt @@ -0,0 +1,51 @@ +/* + * 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 + +import org.springframework.docs.integration.mailusage.OrderManager +import org.springframework.mail.MailException +import org.springframework.mail.MailSender +import org.springframework.mail.SimpleMailMessage + +// tag::snippet[] +class SimpleOrderManager : OrderManager { + + lateinit var mailSender: MailSender + lateinit var templateMessage: SimpleMailMessage + + override fun placeOrder(order: Order) { + // Do the business calculations... + + // Call the collaborators to persist the order... + + // Create a thread-safe "copy" of the template message and customize it + + val msg = SimpleMailMessage(this.templateMessage) + msg.setTo(order.customer.emailAddress) + msg.text = ("Dear " + order.customer.firstName + + order.customer.lastName + + ", thank you for placing order. Your order number is " + + order.orderNumber) + try { + mailSender.send(msg) + } catch (ex: MailException) { + // simply log it and go on... + System.err.println(ex.message) + } + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.kt new file mode 100644 index 000000000000..41b42517b9c1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2025 the original author 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.resthttpinterface.customresolver + +import org.springframework.core.MethodParameter +import org.springframework.web.client.RestClient +import org.springframework.web.client.support.RestClientAdapter +import org.springframework.web.service.annotation.GetExchange +import org.springframework.web.service.invoker.HttpRequestValues +import org.springframework.web.service.invoker.HttpServiceArgumentResolver +import org.springframework.web.service.invoker.HttpServiceProxyFactory + +class CustomHttpServiceArgumentResolver { + + // tag::httpinterface[] + interface RepositoryService { + + @GetExchange("/repos/search") + fun searchRepository(search: Search): List + + } + // end::httpinterface[] + + class Sample { + fun sample() { + // tag::usage[] + val restClient = RestClient.builder().baseUrl("https://api.github.com/").build() + val adapter = RestClientAdapter.create(restClient) + val factory = HttpServiceProxyFactory + .builderFor(adapter) + .customArgumentResolver(SearchQueryArgumentResolver()) + .build() + val repositoryService = factory.createClient(RepositoryService::class.java) + + val search = Search(owner = "spring-projects", language = "java", query = "rest") + val repositories = repositoryService.searchRepository(search) + // end::usage[] + repositories.size + } + } + + // tag::argumentresolver[] + class SearchQueryArgumentResolver : HttpServiceArgumentResolver { + override fun resolve( + argument: Any?, + parameter: MethodParameter, + requestValues: HttpRequestValues.Builder + ): Boolean { + if (parameter.getParameterType() == Search::class.java) { + val search = argument as Search + requestValues.addRequestParameter("owner", search.owner) + .addRequestParameter("language", search.language) + .addRequestParameter("query", search.query) + return true + } + return false + } + } + // end::argumentresolver[] + + data class Search(val query: String, val owner: String, val language: String) + + data class Repository(val name: String) +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingenableannotationsupport/SchedulingConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingenableannotationsupport/SchedulingConfiguration.kt new file mode 100644 index 000000000000..4faed328fb37 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingenableannotationsupport/SchedulingConfiguration.kt @@ -0,0 +1,28 @@ +/* + * 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.schedulingenableannotationsupport + +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.annotation.EnableAsync +import org.springframework.scheduling.annotation.EnableScheduling + +// tag::snippet[] +@Configuration +@EnableAsync +@EnableScheduling +class SchedulingConfiguration +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/LoggingTaskDecorator.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/LoggingTaskDecorator.kt new file mode 100644 index 000000000000..a9501bf7b83d --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/LoggingTaskDecorator.kt @@ -0,0 +1,38 @@ +/* + * 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.schedulingtaskexecutorusage + +import org.apache.commons.logging.Log +import org.apache.commons.logging.LogFactory +import org.springframework.core.task.TaskDecorator + +class LoggingTaskDecorator : TaskDecorator { + + override fun decorate(runnable: Runnable): Runnable { + return Runnable { + logger.debug("Before execution of $runnable") + runnable.run() + logger.debug("After execution of $runnable") + } + } + + companion object { + private val logger: Log = LogFactory.getLog( + LoggingTaskDecorator::class.java + ) + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorConfiguration.kt new file mode 100644 index 000000000000..dea4510ac937 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorConfiguration.kt @@ -0,0 +1,44 @@ +/* + * 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.schedulingtaskexecutorusage + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor + +@Configuration +class TaskExecutorConfiguration { + + // tag::snippet[] + @Bean + fun taskExecutor() = ThreadPoolTaskExecutor().apply { + corePoolSize = 5 + maxPoolSize = 10 + queueCapacity = 25 + } + + @Bean + fun taskExecutorExample(taskExecutor: ThreadPoolTaskExecutor) = TaskExecutorExample(taskExecutor) + // end::snippet[] + + // tag::decorator[] + @Bean + fun decoratedTaskExecutor() = ThreadPoolTaskExecutor().apply { + setTaskDecorator(LoggingTaskDecorator()) + } + // end::decorator[] +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorExample.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorExample.kt new file mode 100644 index 000000000000..4ec8a261dd9e --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorExample.kt @@ -0,0 +1,40 @@ +/* + * 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.schedulingtaskexecutorusage + +import org.springframework.core.task.TaskExecutor + +// tag::snippet[] +class TaskExecutorExample(private val taskExecutor: TaskExecutor) { + + private inner class MessagePrinterTask(private val message: String) : Runnable { + override fun run() { + println(message) + } + } + + fun printMessages() { + for (i in 0..24) { + taskExecutor.execute( + MessagePrinterTask( + "Message$i" + ) + ) + } + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertions/HotelControllerTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertions/HotelControllerTests.kt new file mode 100644 index 000000000000..851eb323ccac --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertions/HotelControllerTests.kt @@ -0,0 +1,48 @@ +/* + * 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.mockmvctesterassertions + +import org.assertj.core.api.Assertions.assertThat +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.assertj.MockMvcTester + +class HotelControllerTests { + + private val mockMvc = MockMvcTester.of(HotelController()) + + fun getHotel() { + // tag::get[] + assertThat(mockMvc.get().uri("/hotels/{id}", 42)) + .hasStatusOk() + .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .bodyJson().isLenientlyEqualTo("sample/hotel-42.json") + // end::get[] + } + + + fun getHotelInvalid() { + // tag::failure[] + assertThat(mockMvc.get().uri("/hotels/{id}", -1)) + .hasFailed() + .hasStatus(HttpStatus.BAD_REQUEST) + .failure().hasMessageContaining("Identifier should be positive") + // end::failure[] + } + + class HotelController +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertionsjson/FamilyControllerTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertionsjson/FamilyControllerTests.kt new file mode 100644 index 000000000000..71faa576fc75 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertionsjson/FamilyControllerTests.kt @@ -0,0 +1,76 @@ +/* + * 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.mockmvctesterassertionsjson + +import org.assertj.core.api.Assertions.* +import org.assertj.core.api.InstanceOfAssertFactories +import org.assertj.core.api.ThrowingConsumer +import org.springframework.test.web.servlet.assertj.MockMvcTester + +/** + * + * @author Stephane Nicoll + */ +class FamilyControllerTests { + + private val mockMvc = MockMvcTester.of(FamilyController()) + + + fun extractingPathAsMap() { + // tag::extract-asmap[] + assertThat(mockMvc.get().uri("/family")).bodyJson() + .extractingPath("$.members[0]") + .asMap() + .contains(entry("name", "Homer")) + // end::extract-asmap[] + } + + fun extractingPathAndConvertWithType() { + // tag::extract-convert[] + assertThat(mockMvc.get().uri("/family")).bodyJson() + .extractingPath("$.members[0]") + .convertTo(Member::class.java) + .satisfies(ThrowingConsumer { member: Member -> + assertThat(member.name).isEqualTo("Homer") + }) + // end::extract-convert[] + } + + fun extractingPathAndConvertWithAssertFactory() { + // tag::extract-convert-assert-factory[] + assertThat(mockMvc.get().uri("/family")).bodyJson() + .extractingPath("$.members") + .convertTo(InstanceOfAssertFactories.list(Member::class.java)) + .hasSize(5) + .element(0).satisfies(ThrowingConsumer { member: Member -> + assertThat(member.name).isEqualTo("Homer") + }) + // end::extract-convert-assert-factory[] + } + + fun assertTheSimpsons() { + // tag::assert-file[] + assertThat(mockMvc.get().uri("/family")).bodyJson() + .isStrictlyEqualTo("sample/simpsons.json") + // end::assert-file[] + } + + class FamilyController + + @JvmRecord + data class Member(val name: String) +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterintegration/HotelController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterintegration/HotelController.kt new file mode 100644 index 000000000000..923c3557a5f7 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterintegration/HotelController.kt @@ -0,0 +1,53 @@ +/* + * 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.mockmvctesterintegration + +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.* +import org.springframework.test.web.servlet.assertj.MockMvcTester +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +/** + * + * @author Stephane Nicoll + */ +class HotelController { + + private val mockMvc = MockMvcTester.of(HotelController()) + + + fun perform() { + // tag::perform[] + // Static import on MockMvcRequestBuilders.get + assertThat(mockMvc.perform(get("/hotels/{id}",42))) + .hasStatusOk() + // end::perform[] + } + + fun performWithCustomMatcher() { + // tag::perform[] + // Static import on MockMvcResultMatchers.status + assertThat(mockMvc.get().uri("/hotels/{id}", 42)) + .matches(status().isOk()) + // end::perform[] + } + + class HotelController +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequests/HotelControllerTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequests/HotelControllerTests.kt new file mode 100644 index 000000000000..3b0167dc828c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequests/HotelControllerTests.kt @@ -0,0 +1,62 @@ +/* + * 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.mockmvctesterrequests + +import org.assertj.core.api.Assertions.assertThat +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.assertj.MockMvcTester + +class HotelControllerTests { + + private val mockMvc = MockMvcTester.of(HotelController()) + + fun createHotel() { + // tag::post[] + assertThat(mockMvc.post().uri("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON)) + . // ... + // end::post[] + hasStatusOk() + } + + fun createHotelMultipleAssertions() { + // tag::post-exchange[] + val result = mockMvc.post().uri("/hotels/{id}", 42) + .accept(MediaType.APPLICATION_JSON).exchange() + assertThat(result) + . // ... + // end::post-exchange[] + hasStatusOk() + } + + fun queryParameters() { + // tag::query-parameters[] + assertThat(mockMvc.get().uri("/hotels?thing={thing}", "somewhere")) + . // ... + //end::query-parameters[] + hasStatusOk() + } + + fun parameters() { + // tag::parameters[] + assertThat(mockMvc.get().uri("/hotels").param("thing", "somewhere")) + . // ... + // end::parameters[] + hasStatusOk() + } + + class HotelController +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsasync/AsyncControllerTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsasync/AsyncControllerTests.kt new file mode 100644 index 000000000000..5dc065b4ddb6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsasync/AsyncControllerTests.kt @@ -0,0 +1,36 @@ +/* + * 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.mockmvctesterrequestsasync + +import org.assertj.core.api.Assertions.assertThat +import org.springframework.test.web.servlet.assertj.MockMvcTester +import java.time.Duration + +class AsyncControllerTests { + + private val mockMvc = MockMvcTester.of(AsyncController()) + + fun asyncExchangeWithCustomTimeToWait() { + // tag::duration[] + assertThat(mockMvc.get().uri("/compute").exchange(Duration.ofSeconds(5))) + . // ... + // end::duration[] + hasStatusOk() + } + + class AsyncController +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsmultipart/MultipartControllerTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsmultipart/MultipartControllerTests.kt new file mode 100644 index 000000000000..07762bd7c6bb --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsmultipart/MultipartControllerTests.kt @@ -0,0 +1,42 @@ +/* + * 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.mockmvctesterrequestsmultipart + +import org.assertj.core.api.Assertions.assertThat +import org.springframework.test.web.servlet.assertj.MockMvcTester +import java.nio.charset.StandardCharsets + +/** + * + * @author Stephane Nicoll + */ +class MultipartControllerTests { + + private val mockMvc = MockMvcTester.of(MultipartController()) + + fun multiPart() { + // tag::snippet[] + assertThat(mockMvc.post().uri("/upload").multipart() + .file("file1.txt", "Hello".toByteArray(StandardCharsets.UTF_8)) + .file("file2.txt", "World".toByteArray(StandardCharsets.UTF_8))) + . // ... + // end::snippet[] + hasStatusOk() + } + + class MultipartController +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestspaths/HotelControllerTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestspaths/HotelControllerTests.kt new file mode 100644 index 000000000000..2be16eab1114 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestspaths/HotelControllerTests.kt @@ -0,0 +1,54 @@ +/* + * 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.mockmvctesterrequestspaths + +import org.assertj.core.api.Assertions.assertThat +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.assertj.MockMvcTester +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder + +class HotelControllerTests { + + private val mockMvc = MockMvcTester.of(HotelController()) + + fun contextAndServletPaths() { + // tag::context-servlet-paths[] + assertThat(mockMvc.get().uri("/app/main/hotels/{id}", 42) + .contextPath("/app").servletPath("/main")) + . // ... + // end::context-servlet-paths[] + hasStatusOk() + } + + fun configureMockMvcTesterWithDefaultSettings() { + // tag::default-customizations[] + val mockMvc = + MockMvcTester.of(listOf(HotelController())) { builder: StandaloneMockMvcBuilder -> + builder.defaultRequest( + MockMvcRequestBuilders.get("/") + .contextPath("/app").servletPath("/main") + .accept(MediaType.APPLICATION_JSON) + ).build() + } + // end::default-customizations[] + mockMvc.toString() // avoid warning + } + + + class HotelController +} \ 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/AccountControllerIntegrationTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerIntegrationTests.kt new file mode 100644 index 000000000000..5f86ee6c8e05 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerIntegrationTests.kt @@ -0,0 +1,34 @@ +/* + * 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.beans.factory.annotation.Autowired +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig +import org.springframework.test.web.servlet.assertj.MockMvcTester +import org.springframework.web.context.WebApplicationContext + +// tag::snippet[] +@SpringJUnitWebConfig(ApplicationWebConfiguration::class) +class AccountControllerIntegrationTests(@Autowired wac: WebApplicationContext) { + + private val mockMvc = MockMvcTester.from(wac) + + // ... + +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerStandaloneTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerStandaloneTests.kt new file mode 100644 index 000000000000..2d10ec032aa9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerStandaloneTests.kt @@ -0,0 +1,29 @@ +/* + * 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.test.web.servlet.assertj.MockMvcTester + +// tag::snippet[] +class AccountControllerStandaloneTests { + + val mockMvc = MockMvcTester.of(AccountController()) + + // ... + +} +// end::snippet[] \ 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 new file mode 100644 index 000000000000..523d728941e4 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.kt @@ -0,0 +1,37 @@ +/* + * 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.converter + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.ApplicationWebConfiguration +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig +import org.springframework.test.web.servlet.assertj.MockMvcTester +import org.springframework.web.context.WebApplicationContext + +// tag::snippet[] +@SpringJUnitWebConfig(ApplicationWebConfiguration::class) +class AccountControllerIntegrationTests(@Autowired wac: WebApplicationContext) { + + private val mockMvc = MockMvcTester.from(wac).withHttpMessageConverters( + listOf(wac.getBean(AbstractJackson2HttpMessageConverter::class.java))) + + // ... + +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/webfluxanncontrollerexceptions/SimpleController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/webfluxanncontrollerexceptions/SimpleController.kt new file mode 100644 index 000000000000..645aa1de0b4f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/webfluxanncontrollerexceptions/SimpleController.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webflux.controller.webfluxanncontrollerexceptions; + +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.ExceptionHandler +import java.io.IOException + +@Controller +class SimpleController { + + @ExceptionHandler(IOException::class) + fun handle() : ResponseEntity { + return ResponseEntity.internalServerError().body("Could not read file storage") + } + +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/webfluxannexceptionhandlermedia/MediaTypeController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/webfluxannexceptionhandlermedia/MediaTypeController.kt new file mode 100644 index 000000000000..3bddcc33f21a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/webfluxannexceptionhandlermedia/MediaTypeController.kt @@ -0,0 +1,42 @@ +/* + * 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.webflux.controller.webfluxannexceptionhandlermedia + +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.ExceptionHandler + +@Controller +class MediaTypeController { + + // tag::mediatype[] + @ExceptionHandler(produces = ["application/json"]) + fun handleJson(exc: IllegalArgumentException): ResponseEntity { + return ResponseEntity.badRequest().body(ErrorMessage(exc.message, 42)) + } + + @ExceptionHandler(produces = ["text/html"]) + fun handle(exc: IllegalArgumentException, model: Model): String { + model.addAttribute("error", ErrorMessage(exc.message, 42)) + return "errorView" + } + // end::mediatype[] + + @JvmRecord + data class ErrorMessage(val message: String?, val code: Int) +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/filters/urlhandler/UrlHandlerFilterConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/filters/urlhandler/UrlHandlerFilterConfiguration.kt new file mode 100644 index 000000000000..bcfc050b1f41 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/filters/urlhandler/UrlHandlerFilterConfiguration.kt @@ -0,0 +1,35 @@ +/* + * 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.webflux.filters.urlhandler + +import org.springframework.http.HttpStatus +import org.springframework.web.filter.reactive.UrlHandlerFilter + +class UrlHandlerFilterConfiguration { + + @Suppress("UNUSED_VARIABLE") + fun configureUrlHandlerFilter() { + // tag::config[] + val urlHandlerFilter = UrlHandlerFilter + // will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post" + .trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT) + // will mutate the request to "/admin/user/account/" and make it as "/admin/user/account" + .trailingSlashHandler("/admin/**").mutateRequest() + .build() + // end::config[] + } +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.kt new file mode 100644 index 000000000000..3194264eba6b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webflux.webfluxconfigpathmatching + +import org.springframework.context.annotation.Configuration +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.method.HandlerTypePredicate +import org.springframework.web.reactive.config.PathMatchConfigurer +import org.springframework.web.reactive.config.WebFluxConfigurer + +@Configuration +class WebConfig : WebFluxConfigurer { + + override fun configurePathMatching(configurer: PathMatchConfigurer) { + configurer.addPathPrefix( + "/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) + } +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/filters/urlhandler/UrlHandlerFilterConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/filters/urlhandler/UrlHandlerFilterConfiguration.kt new file mode 100644 index 000000000000..9714924e3737 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/filters/urlhandler/UrlHandlerFilterConfiguration.kt @@ -0,0 +1,35 @@ +/* + * 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.filters.urlhandler + +import org.springframework.http.HttpStatus +import org.springframework.web.filter.UrlHandlerFilter + +class UrlHandlerFilterConfiguration { + + @Suppress("UNUSED_VARIABLE") + fun configureUrlHandlerFilter() { + // tag::config[] + val urlHandlerFilter = UrlHandlerFilter + // will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post" + .trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT) + // will wrap the request to "/admin/user/account/" and make it as "/admin/user/account" + .trailingSlashHandler("/admin/**").wrapRequest() + .build() + // end::config[] + } +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.kt new file mode 100644 index 000000000000..f5ee4887eb8f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.kt @@ -0,0 +1,28 @@ +/* + * 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.mvcconfigadvancedjava + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration + +// tag::snippet[] +@Configuration +class WebConfiguration : DelegatingWebMvcConfiguration() { + + // ... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt new file mode 100644 index 000000000000..c5628f27de41 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigadvancedxml + +import org.springframework.beans.factory.config.BeanPostProcessor +import org.springframework.stereotype.Component + +// tag::snippet[] +@Component +class MyPostProcessor : BeanPostProcessor { + + override fun postProcessBeforeInitialization(bean: Any, name: String): Any { + // ... + return bean + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.kt new file mode 100644 index 000000000000..50bd075660bf --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.kt @@ -0,0 +1,33 @@ +/* + * 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.mvcconfigcontentnegotiation + +import org.springframework.context.annotation.Configuration +import org.springframework.http.MediaType +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) { + configurer.mediaType("json", MediaType.APPLICATION_JSON) + configurer.mediaType("xml", MediaType.APPLICATION_XML) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.kt new file mode 100644 index 000000000000..f77e14982ce9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.kt @@ -0,0 +1,36 @@ +/* + * 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.mvcconfigconversion + +import org.springframework.context.annotation.Configuration +import org.springframework.format.FormatterRegistry +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + + +// tag::snippet[] +@Configuration +class DateTimeWebConfiguration : WebMvcConfigurer { + + override fun addFormatters(registry: FormatterRegistry) { + DateTimeFormatterRegistrar().apply { + setUseIsoFormat(true) + registerFormatters(registry) + } + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.kt new file mode 100644 index 000000000000..534fa04b8cdc --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigconversion + +import org.springframework.context.annotation.Configuration +import org.springframework.format.FormatterRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun addFormatters(registry: FormatterRegistry) { + // ... + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.kt new file mode 100644 index 000000000000..485b9f71e02a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.kt @@ -0,0 +1,28 @@ +/* + * 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.mvcconfigcustomize + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + // Implement configuration methods... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.kt new file mode 100644 index 000000000000..5fe920100b57 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.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.mvcconfigenable + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +// tag::snippet[] +@Configuration +@EnableWebMvc +class WebConfiguration { +} +// 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 new file mode 100644 index 000000000000..9bbb738f3abe --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.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 + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(LocaleChangeInterceptor()) + 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 new file mode 100644 index 000000000000..12c197a46f51 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt @@ -0,0 +1,25 @@ +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigmessageconverters + +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule +import org.springframework.context.annotation.Configuration +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import java.text.SimpleDateFormat + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureMessageConverters(converters: MutableList>) { + val builder = Jackson2ObjectMapperBuilder() + .indentOutput(true) + .dateFormat(SimpleDateFormat("yyyy-MM-dd")) + .modulesToInstall(ParameterNamesModule()) + converters.add(MappingJackson2HttpMessageConverter(builder.build())) + converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt new file mode 100644 index 000000000000..1ee4be3095cd --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt @@ -0,0 +1,40 @@ +/* + * 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.mvcconfigpathmatching + +import org.springframework.context.annotation.Configuration +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.method.HandlerTypePredicate +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.util.pattern.PathPatternParser + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configurePathMatch(configurer: PathMatchConfigurer) { + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) + } + + fun patternParser(): PathPatternParser { + val pathPatternParser = PathPatternParser() + //... + return pathPatternParser + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.kt new file mode 100644 index 000000000000..5cb39227e946 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.kt @@ -0,0 +1,35 @@ +/* + * 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.mvcconfigstaticresources + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.resource.VersionResourceResolver + +// tag::snippet[] +@Configuration +class VersionedConfiguration : WebMvcConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public/") + .resourceChain(true) + .addResolver(VersionResourceResolver().addContentVersionStrategy("/**")) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.kt new file mode 100644 index 000000000000..72c91e87f177 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.kt @@ -0,0 +1,35 @@ +/* + * 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.mvcconfigstaticresources + +import org.springframework.context.annotation.Configuration +import org.springframework.http.CacheControl +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import java.time.Duration + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))) + } +} +// end::snippet[] 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/mvcconfigvalidation/MyController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.kt new file mode 100644 index 000000000000..6d2522c48d47 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigvalidation + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.WebDataBinder +import org.springframework.web.bind.annotation.InitBinder + +// tag::snippet[] +@Controller +class MyController { + + @InitBinder + fun initBinder(binder: WebDataBinder) { + binder.addValidators(FooValidator()) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt new file mode 100644 index 000000000000..23f2f5d4a267 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.context.annotation.Configuration +import org.springframework.validation.Validator +import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun getValidator(): Validator { + val validator = OptionalValidatorFactoryBean() + // ... + return validator + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.kt new file mode 100644 index 000000000000..69ade9721866 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewcontroller + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun addViewControllers(registry: ViewControllerRegistry) { + registry.addViewController("/").setViewName("home") + } +} +// end::snippet[] 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 new file mode 100644 index 000000000000..55acaa63cb11 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt @@ -0,0 +1,24 @@ +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer +import org.springframework.web.servlet.view.json.MappingJackson2JsonView + +// tag::snippet[] +@Configuration +class FreeMarkerConfiguration : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.enableContentNegotiation(MappingJackson2JsonView()) + registry.freeMarker().cache(false) + } + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("/freemarker") + } +} +// end::snippet[] \ No newline at end of file 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 new file mode 100644 index 000000000000..472ecf25bf30 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.view.json.MappingJackson2JsonView + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.enableContentNegotiation(MappingJackson2JsonView()) + registry.jsp() + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.kt new file mode 100644 index 000000000000..648beac23750 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcdefaultservlethandler + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class CustomDefaultServletConfiguration : WebMvcConfigurer { + + override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { + configurer.enable("myCustomDefaultServlet") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.kt new file mode 100644 index 000000000000..a217953aa0fd --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.kt @@ -0,0 +1,30 @@ +/* + * 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.mvcdefaultservlethandler + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { + configurer.enable() + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.kt new file mode 100644 index 000000000000..410b77ff06f4 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.kt @@ -0,0 +1,29 @@ +/* + * 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.mvccontroller.mvcanncontroller + +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + +// tag::snippet[] +@Configuration +@ComponentScan("org.example.web") +class WebConfiguration { + + // ... +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandler/SimpleController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandler/SimpleController.kt new file mode 100644 index 000000000000..f89f36d05977 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandler/SimpleController.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvccontroller.mvcannexceptionhandler; + +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.ExceptionHandler +import java.io.IOException + +@Controller +class SimpleController { + + @ExceptionHandler(IOException::class) + fun handle() : ResponseEntity { + return ResponseEntity.internalServerError().body("Could not read file storage") + } + +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlerexc/ExceptionController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlerexc/ExceptionController.kt new file mode 100644 index 000000000000..548ed6e56814 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlerexc/ExceptionController.kt @@ -0,0 +1,42 @@ +/* + * 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.mvccontroller.mvcannexceptionhandlerexc + +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.ExceptionHandler +import java.io.IOException +import java.rmi.RemoteException + +@Controller +class ExceptionController { + + // tag::narrow[] + @ExceptionHandler(FileSystemException::class, RemoteException::class) + fun handleIoException(ex: IOException): ResponseEntity { + return ResponseEntity.internalServerError().body(ex.message) + } + // end::narrow[] + + + // tag::general[] + @ExceptionHandler(FileSystemException::class, RemoteException::class) + fun handleExceptions(ex: Exception): ResponseEntity { + return ResponseEntity.internalServerError().body(ex.message) + } + // end::general[] +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlermedia/MediaTypeController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlermedia/MediaTypeController.kt new file mode 100644 index 000000000000..e1311de33ea3 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannexceptionhandlermedia/MediaTypeController.kt @@ -0,0 +1,42 @@ +/* + * 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.mvccontroller.mvcannexceptionhandlermedia + +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.ExceptionHandler + +@Controller +class MediaTypeController { + + // tag::mediatype[] + @ExceptionHandler(produces = ["application/json"]) + fun handleJson(exc: IllegalArgumentException): ResponseEntity { + return ResponseEntity.badRequest().body(ErrorMessage(exc.message, 42)) + } + + @ExceptionHandler(produces = ["text/html"]) + fun handle(exc: IllegalArgumentException, model: Model): String { + model.addAttribute("error", ErrorMessage(exc.message, 42)) + return "errorView" + } + // end::mediatype[] + + @JvmRecord + data class ErrorMessage(val message: String?, val code: Int) +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompauthenticationtokenbased/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompauthenticationtokenbased/WebSocketConfiguration.kt new file mode 100644 index 000000000000..d03e7e0995d4 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompauthenticationtokenbased/WebSocketConfiguration.kt @@ -0,0 +1,48 @@ +/* + * 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.stomp.websocketstompauthenticationtokenbased + +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.Message +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.simp.config.ChannelRegistration +import org.springframework.messaging.simp.stomp.StompCommand +import org.springframework.messaging.simp.stomp.StompHeaderAccessor +import org.springframework.messaging.support.ChannelInterceptor +import org.springframework.messaging.support.MessageHeaderAccessor +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun configureClientInboundChannel(registration: ChannelRegistration) { + registration.interceptors(object : ChannelInterceptor { + override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> { + val accessor = MessageHeaderAccessor.getAccessor(message, + StompHeaderAccessor::class.java) + if (StompCommand.CONNECT == accessor!!.command) { + // Access authentication header(s) and invoke accessor.setUser(user) + } + return message + } + }) + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/MessageSizeLimitWebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/MessageSizeLimitWebSocketConfiguration.kt new file mode 100644 index 000000000000..b8affe8c5ee0 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/MessageSizeLimitWebSocketConfiguration.kt @@ -0,0 +1,35 @@ +/* + * 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.stomp.websocketstompconfigurationperformance + +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class MessageSizeLimitWebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun configureWebSocketTransport(registration: WebSocketTransportRegistration) { + registration.setMessageSizeLimit(128 * 1024) + } + + // ... +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/WebSocketConfiguration.kt new file mode 100644 index 000000000000..ecdd126a33ff --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/WebSocketConfiguration.kt @@ -0,0 +1,35 @@ +/* + * 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.stomp.websocketstompconfigurationperformance + +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun configureWebSocketTransport(registration: WebSocketTransportRegistration) { + registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024) + } + + // ... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/RedController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/RedController.kt new file mode 100644 index 000000000000..88ebf88248fa --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/RedController.kt @@ -0,0 +1,34 @@ +/* + * 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. + */ + +@file:Suppress("UNUSED_PARAMETER") +package org.springframework.docs.web.websocket.stomp.websocketstompdestinationseparator + +import org.springframework.messaging.handler.annotation.DestinationVariable +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.stereotype.Controller + +// tag::snippet[] +@Controller +@MessageMapping("red") +class RedController { + + @MessageMapping("blue.{green}") + fun handleGreen(@DestinationVariable green: String) { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/WebSocketConfiguration.kt new file mode 100644 index 000000000000..0e0502a6501c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/WebSocketConfiguration.kt @@ -0,0 +1,38 @@ +/* + * 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.stomp.websocketstompdestinationseparator + +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.util.AntPathMatcher +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + // ... + + override fun configureMessageBroker(registry: MessageBrokerRegistry) { + registry.setPathMatcher(AntPathMatcher(".")) + registry.enableStompBrokerRelay("/queue", "/topic") + registry.setApplicationDestinationPrefixes("/app") + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompenable/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompenable/WebSocketConfiguration.kt new file mode 100644 index 000000000000..554d70962155 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompenable/WebSocketConfiguration.kt @@ -0,0 +1,45 @@ +/* + * 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.stomp.websocketstompenable + +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.StompEndpointRegistry +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + // /portfolio is the HTTP URL for the endpoint to which a WebSocket (or SockJS) + // client needs to connect for the WebSocket handshake + registry.addEndpoint("/portfolio") + } + + override fun configureMessageBroker(config: MessageBrokerRegistry) { + // STOMP messages whose destination header begins with /app are routed to + // @MessageMapping methods in @Controller classes + config.setApplicationDestinationPrefixes("/app") + // Use the built-in message broker for subscriptions and broadcasting and + // route messages whose destination header begins with /topic or /queue to the broker + config.enableSimpleBroker("/topic", "/queue") + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelay/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelay/WebSocketConfiguration.kt new file mode 100644 index 000000000000..aa3630b5c693 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelay/WebSocketConfiguration.kt @@ -0,0 +1,38 @@ +/* + * 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.stomp.websocketstomphandlebrokerrelay + +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.StompEndpointRegistry +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + registry.addEndpoint("/portfolio").withSockJS() + } + + override fun configureMessageBroker(registry: MessageBrokerRegistry) { + registry.enableStompBrokerRelay("/topic", "/queue") + registry.setApplicationDestinationPrefixes("/app") + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelayconfigure/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelayconfigure/WebSocketConfiguration.kt new file mode 100644 index 000000000000..1fb61d28b8bb --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelayconfigure/WebSocketConfiguration.kt @@ -0,0 +1,44 @@ +/* + * 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. + */ + +@file:Suppress("DEPRECATION") +package org.springframework.docs.web.websocket.stomp.websocketstomphandlebrokerrelayconfigure + +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.messaging.simp.stomp.StompReactorNettyCodec +import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer +import java.net.InetSocketAddress + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + // ... + + override fun configureMessageBroker(registry: MessageBrokerRegistry) { + registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient()) + registry.setApplicationDestinationPrefixes("/app") + } + + private fun createTcpClient(): ReactorNettyTcpClient { + return ReactorNettyTcpClient({ it.addressSupplier { InetSocketAddress(0) } }, StompReactorNettyCodec()) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomphandlesimplebroker/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomphandlesimplebroker/WebSocketConfiguration.kt new file mode 100644 index 000000000000..4602aaee2089 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomphandlesimplebroker/WebSocketConfiguration.kt @@ -0,0 +1,47 @@ +/* + * 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.stomp.websocketstomphandlesimplebroker + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Lazy +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.scheduling.TaskScheduler +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + private lateinit var messageBrokerTaskScheduler: TaskScheduler + + @Autowired + fun setMessageBrokerTaskScheduler(@Lazy taskScheduler: TaskScheduler) { + this.messageBrokerTaskScheduler = taskScheduler + } + + override fun configureMessageBroker(registry: MessageBrokerRegistry) { + registry.enableSimpleBroker("/queue/", "/topic/") + .setHeartbeatValue(longArrayOf(10000, 20000)) + .setTaskScheduler(messageBrokerTaskScheduler) + + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/MyChannelInterceptor.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/MyChannelInterceptor.kt new file mode 100644 index 000000000000..45c7f93e1301 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/MyChannelInterceptor.kt @@ -0,0 +1,35 @@ +/* + * 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. + */ + +@file:Suppress("UNUSED_VARIABLE") +package org.springframework.docs.web.websocket.stomp.websocketstompinterceptors + +import org.springframework.messaging.Message +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.simp.stomp.StompHeaderAccessor +import org.springframework.messaging.support.ChannelInterceptor + +// tag::snippet[] +class MyChannelInterceptor : ChannelInterceptor { + + override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> { + val accessor = StompHeaderAccessor.wrap(message) + val command = accessor.command + // ... + return message + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/WebSocketConfiguration.kt new file mode 100644 index 000000000000..2516b8d73018 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompinterceptors/WebSocketConfiguration.kt @@ -0,0 +1,33 @@ +/* + * 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.stomp.websocketstompinterceptors + +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.ChannelRegistration +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun configureClientInboundChannel(registration: ChannelRegistration) { + registration.interceptors(MyChannelInterceptor()) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/GreetingController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/GreetingController.kt new file mode 100644 index 000000000000..ab24a3f16d87 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/GreetingController.kt @@ -0,0 +1,21 @@ +package org.springframework.docs.web.websocket.stomp.websocketstompmessageflow + +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.stereotype.Controller +import java.text.SimpleDateFormat +import java.util.* + +// tag::snippet[] +@Controller +class GreetingController { + + @MessageMapping("/greeting") + fun handle(greeting: String): String { + return "[${getTimestamp()}: $greeting" + } + + private fun getTimestamp(): String { + return SimpleDateFormat("MM/dd/yyyy h:mm:ss a").format(Date()) + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/WebSocketConfiguration.kt new file mode 100644 index 000000000000..e464312da142 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompmessageflow/WebSocketConfiguration.kt @@ -0,0 +1,39 @@ +/* + * 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.stomp.websocketstompmessageflow + +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.StompEndpointRegistry +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + registry.addEndpoint("/portfolio") + } + + override fun configureMessageBroker(registry: MessageBrokerRegistry) { + registry.setApplicationDestinationPrefixes("/app") + registry.enableSimpleBroker("/topic") + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/PublishOrderWebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/PublishOrderWebSocketConfiguration.kt new file mode 100644 index 000000000000..74c6d508e6bf --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/PublishOrderWebSocketConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.stomp.websocketstomporderedmessages + +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class PublishOrderWebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun configureMessageBroker(registry: MessageBrokerRegistry) { + // ... + registry.setPreservePublishOrder(true) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/ReceiveOrderWebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/ReceiveOrderWebSocketConfiguration.kt new file mode 100644 index 000000000000..a5324bbe0b2c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/ReceiveOrderWebSocketConfiguration.kt @@ -0,0 +1,33 @@ +/* + * 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.stomp.websocketstomporderedmessages + +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.StompEndpointRegistry +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class ReceiveOrderWebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + registry.setPreserveReceiveOrder(true) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/JettyWebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/JettyWebSocketConfiguration.kt new file mode 100644 index 000000000000..75420c84ac44 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/JettyWebSocketConfiguration.kt @@ -0,0 +1,47 @@ +/* + * 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.stomp.websocketstompserverconfig + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.StompEndpointRegistry +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer +import org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy +import org.springframework.web.socket.server.support.DefaultHandshakeHandler +import java.time.Duration + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class JettyWebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler()) + } + + @Bean + fun handshakeHandler(): DefaultHandshakeHandler { + val strategy = JettyRequestUpgradeStrategy() + strategy.addWebSocketConfigurer { + it.inputBufferSize = 4 * 8192 + it.idleTimeout = Duration.ofSeconds(600) + } + return DefaultHandshakeHandler(strategy) + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/WebSocketConfiguration.kt new file mode 100644 index 000000000000..50b6916fe866 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/stomp/websocketstompserverconfig/WebSocketConfiguration.kt @@ -0,0 +1,34 @@ +/* + * 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.stomp.websocketstompserverconfig + +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration + +// tag::snippet[] +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfiguration : WebSocketMessageBrokerConfigurer { + + override fun configureWebSocketTransport(registry: WebSocketTransportRegistration) { + registry.setMessageSizeLimit(4 * 8192) + registry.setTimeToFirstMessage(30000) + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketfallbacksockjsenable/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketfallbacksockjsenable/WebSocketConfiguration.kt new file mode 100644 index 000000000000..432e8044882f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketfallbacksockjsenable/WebSocketConfiguration.kt @@ -0,0 +1,39 @@ +/* + * 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.websocketfallbacksockjsenable + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.docs.web.websocket.websocketserverhandler.MyHandler +import org.springframework.web.socket.WebSocketHandler +import org.springframework.web.socket.config.annotation.EnableWebSocket +import org.springframework.web.socket.config.annotation.WebSocketConfigurer +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry + +// tag::snippet[] +@Configuration +@EnableWebSocket +class WebSocketConfiguration : WebSocketConfigurer { + override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) { + registry.addHandler(myHandler(), "/myHandler").withSockJS() + } + + @Bean + fun myHandler(): WebSocketHandler { + return MyHandler() + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverallowedorigins/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverallowedorigins/WebSocketConfiguration.kt new file mode 100644 index 000000000000..79f80e690df1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverallowedorigins/WebSocketConfiguration.kt @@ -0,0 +1,40 @@ +/* + * 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.websocketserverallowedorigins + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.docs.web.websocket.websocketserverhandler.MyHandler +import org.springframework.web.socket.WebSocketHandler +import org.springframework.web.socket.config.annotation.EnableWebSocket +import org.springframework.web.socket.config.annotation.WebSocketConfigurer +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry + +// tag::snippet[] +@Configuration +@EnableWebSocket +class WebSocketConfiguration : WebSocketConfigurer { + + override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) { + registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com") + } + + @Bean + fun myHandler(): WebSocketHandler { + return MyHandler() + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverhandler/MyHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverhandler/MyHandler.kt new file mode 100644 index 000000000000..38b53e01b646 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverhandler/MyHandler.kt @@ -0,0 +1,30 @@ +/* + * 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.websocketserverhandler + +import org.springframework.web.socket.TextMessage +import org.springframework.web.socket.WebSocketSession +import org.springframework.web.socket.handler.TextWebSocketHandler + +// tag::snippet[] +class MyHandler : TextWebSocketHandler() { + + override fun handleTextMessage(session: WebSocketSession, message: TextMessage) { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverhandler/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverhandler/WebSocketConfiguration.kt new file mode 100644 index 000000000000..5217d325d165 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverhandler/WebSocketConfiguration.kt @@ -0,0 +1,40 @@ +/* + * 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.websocketserverhandler + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.WebSocketHandler +import org.springframework.web.socket.config.annotation.EnableWebSocket +import org.springframework.web.socket.config.annotation.WebSocketConfigurer +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry + +// tag::snippet[] +@Configuration +@EnableWebSocket +class WebSocketConfiguration : WebSocketConfigurer { + + override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) { + registry.addHandler(myHandler(), "/myHandler") + } + + @Bean + fun myHandler(): WebSocketHandler { + return MyHandler() + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverhandshake/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverhandshake/WebSocketConfiguration.kt new file mode 100644 index 000000000000..fd6d5fd1c62f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverhandshake/WebSocketConfiguration.kt @@ -0,0 +1,36 @@ +/* + * 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.websocketserverhandshake + +import org.springframework.context.annotation.Configuration +import org.springframework.docs.web.websocket.websocketserverhandler.MyHandler +import org.springframework.web.socket.config.annotation.EnableWebSocket +import org.springframework.web.socket.config.annotation.WebSocketConfigurer +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry +import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor + +// tag::snippet[] +@Configuration +@EnableWebSocket +class WebSocketConfiguration : WebSocketConfigurer { + + override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) { + registry.addHandler(MyHandler(), "/myHandler") + .addInterceptors(HttpSessionHandshakeInterceptor()) + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/JettyWebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/JettyWebSocketConfiguration.kt new file mode 100644 index 000000000000..0c2faf491a8f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/JettyWebSocketConfiguration.kt @@ -0,0 +1,52 @@ +/* + * 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.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.WebSocketHandler +import org.springframework.web.socket.config.annotation.EnableWebSocket +import org.springframework.web.socket.config.annotation.WebSocketConfigurer +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry +import org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy +import org.springframework.web.socket.server.support.DefaultHandshakeHandler +import java.time.Duration + +// tag::snippet[] +@Configuration +@EnableWebSocket +class JettyWebSocketConfiguration : WebSocketConfigurer { + + override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) { + registry.addHandler(echoWebSocketHandler(), "/echo").setHandshakeHandler(handshakeHandler()) + } + + @Bean + fun echoWebSocketHandler(): WebSocketHandler { + return MyEchoHandler() + } + + @Bean + fun handshakeHandler(): DefaultHandshakeHandler { + val strategy = JettyRequestUpgradeStrategy() + strategy.addWebSocketConfigurer { + it.inputBufferSize = 8192 + it.idleTimeout = Duration.ofSeconds(600) + } + return DefaultHandshakeHandler(strategy) + } +} +// end::snippet[] 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/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/WebSocketConfiguration.kt new file mode 100644 index 000000000000..32f4357a3eea --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/WebSocketConfiguration.kt @@ -0,0 +1,33 @@ +/* + * 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.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean + +// tag::snippet[] +@Configuration +class WebSocketConfiguration { + + @Bean + fun createWebSocketContainer() = ServletServerContainerFactoryBean().apply { + maxTextMessageBufferSize = 8192 + maxBinaryMessageBufferSize = 8192 + } +} +// end::snippet[] diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/aop/aopajltwspring/ApplicationConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/aop/aopajltwspring/ApplicationConfiguration.xml new file mode 100644 index 000000000000..395a8cfe7c5a --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/aop/aopajltwspring/ApplicationConfiguration.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/aop/aopajltwspring/CustomWeaverConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/aop/aopajltwspring/CustomWeaverConfiguration.xml new file mode 100644 index 000000000000..0837f96099a7 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/aop/aopajltwspring/CustomWeaverConfiguration.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/aop/aopatconfigurable/ApplicationConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/aop/aopatconfigurable/ApplicationConfiguration.xml new file mode 100644 index 000000000000..8472b707e543 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/aop/aopatconfigurable/ApplicationConfiguration.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/aop/ataspectj/aopaspectjsupport/ApplicationConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/aop/ataspectj/aopaspectjsupport/ApplicationConfiguration.xml new file mode 100644 index 000000000000..736824791b81 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/aop/ataspectj/aopaspectjsupport/ApplicationConfiguration.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/aop/ataspectj/aopataspectj/ApplicationConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/aop/ataspectj/aopataspectj/ApplicationConfiguration.xml new file mode 100644 index 000000000000..b591dd381ff1 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/aop/ataspectj/aopataspectj/ApplicationConfiguration.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ApplicationConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ApplicationConfiguration.xml new file mode 100644 index 000000000000..b232cce47330 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/aop/ataspectj/aopataspectjexample/ApplicationConfiguration.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/aopapi/aopapipointcutsregex/JdkRegexpConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/aopapi/aopapipointcutsregex/JdkRegexpConfiguration.xml new file mode 100644 index 000000000000..d008155a71d0 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/aopapi/aopapipointcutsregex/JdkRegexpConfiguration.xml @@ -0,0 +1,20 @@ + + + + + + + + .*set.* + .*absquatulate + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/aopapi/aopapipointcutsregex/RegexpConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/aopapi/aopapipointcutsregex/RegexpConfiguration.xml new file mode 100644 index 000000000000..a4fe96e5d10a --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/aopapi/aopapipointcutsregex/RegexpConfiguration.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + .*set.* + .*absquatulate + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ApplicationConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ApplicationConfiguration.xml new file mode 100644 index 000000000000..bf12e98874db --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ApplicationConfiguration.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/LazyConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/LazyConfiguration.xml new file mode 100644 index 000000000000..45e1ccb184af --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/LazyConfiguration.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/expressions/expressionsbeandef/MovieRecommender.xml b/framework-docs/src/main/resources/org/springframework/docs/core/expressions/expressionsbeandef/MovieRecommender.xml new file mode 100644 index 000000000000..a700e176d812 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/expressions/expressionsbeandef/MovieRecommender.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/expressions/expressionsbeandef/PropertyValueTestBean.xml b/framework-docs/src/main/resources/org/springframework/docs/core/expressions/expressionsbeandef/PropertyValueTestBean.xml new file mode 100644 index 000000000000..d1bdade08dae --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/expressions/expressionsbeandef/PropertyValueTestBean.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/expressions/expressionsbeandef/ShapeGuess.xml b/framework-docs/src/main/resources/org/springframework/docs/core/expressions/expressionsbeandef/ShapeGuess.xml new file mode 100644 index 000000000000..c7703c38eacb --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/expressions/expressionsbeandef/ShapeGuess.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/validation/formatconfiguringformattingglobaldatetimeformat/ApplicationConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/validation/formatconfiguringformattingglobaldatetimeformat/ApplicationConfiguration.xml new file mode 100644 index 000000000000..7e5072e6302b --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/validation/formatconfiguringformattingglobaldatetimeformat/ApplicationConfiguration.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.xml new file mode 100644 index 000000000000..2d0fc9029411 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.xml new file mode 100644 index 000000000000..c7477ce108d4 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml new file mode 100644 index 000000000000..a859292df7e5 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml new file mode 100644 index 000000000000..cf0e6c58a214 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml new file mode 100644 index 000000000000..c385240d733f --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.xml new file mode 100644 index 000000000000..7968814c94f1 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDaoConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDaoConfiguration.xml new file mode 100644 index 000000000000..1feeebf5334b --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventDaoConfiguration.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepositoryConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepositoryConfiguration.xml new file mode 100644 index 000000000000..5f92e7ca5655 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/JdbcCorporateEventRepositoryConfiguration.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cacheannotationenable/CacheConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cacheannotationenable/CacheConfiguration.xml new file mode 100644 index 000000000000..af465a230cc3 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cacheannotationenable/CacheConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CacheConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CacheConfiguration.xml new file mode 100644 index 000000000000..47d890614882 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CacheConfiguration.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.xml new file mode 100644 index 000000000000..9ce1f5af47a0 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.xml @@ -0,0 +1,17 @@ + + + + + + + default + books + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationjdk/CacheConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationjdk/CacheConfiguration.xml new file mode 100644 index 000000000000..3aadd751dc6b --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationjdk/CacheConfiguration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationjsr107/CacheConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationjsr107/CacheConfiguration.xml new file mode 100644 index 000000000000..6231658aa2cb --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationjsr107/CacheConfiguration.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationnoop/CacheConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationnoop/CacheConfiguration.xml new file mode 100644 index 000000000000..a87f3da9f7fd --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/cache/cachestoreconfigurationnoop/CacheConfiguration.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsannotatedsupport/JmsConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsannotatedsupport/JmsConfiguration.xml new file mode 100644 index 000000000000..42d597832ce6 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsannotatedsupport/JmsConfiguration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/AlternativeJmsConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/AlternativeJmsConfiguration.xml new file mode 100644 index 000000000000..2cb1532c67a1 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/AlternativeJmsConfiguration.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/JmsConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/JmsConfiguration.xml new file mode 100644 index 000000000000..a14ad0bff8cf --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsjcamessageendpointmanager/JmsConfiguration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsreceivingasync/JmsConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsreceivingasync/JmsConfiguration.xml new file mode 100644 index 000000000000..c7690e43470a --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsreceivingasync/JmsConfiguration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/JmsConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/JmsConfiguration.xml new file mode 100644 index 000000000000..6471dad0f162 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/JmsConfiguration.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageListenerConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageListenerConfiguration.xml new file mode 100644 index 000000000000..e2fab9723c10 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmsreceivingasyncmessagelisteneradapter/MessageListenerConfiguration.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmstxparticipation/ExternalTxJmsConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmstxparticipation/ExternalTxJmsConfiguration.xml new file mode 100644 index 000000000000..07668236f95e --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmstxparticipation/ExternalTxJmsConfiguration.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmstxparticipation/JmsConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmstxparticipation/JmsConfiguration.xml new file mode 100644 index 000000000000..e0c89287a6af --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jms/jmstxparticipation/JmsConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/CustomJmxConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/CustomJmxConfiguration.xml new file mode 100644 index 000000000000..5a3017cfc845 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/CustomJmxConfiguration.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/JmxConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/JmxConfiguration.xml new file mode 100644 index 000000000000..c1b7fb50d3e2 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jmx/jmxcontextmbeanexport/JmxConfiguration.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/jmx/jmxexporting/JmxConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/jmx/jmxexporting/JmxConfiguration.xml new file mode 100644 index 000000000000..692265fa6027 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/jmx/jmxexporting/JmxConfiguration.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/mailusagesimple/MailConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/mailusagesimple/MailConfiguration.xml new file mode 100644 index 000000000000..a2259b165a19 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/mailusagesimple/MailConfiguration.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/schedulingenableannotationsupport/SchedulingConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/schedulingenableannotationsupport/SchedulingConfiguration.xml new file mode 100644 index 000000000000..aeb0b970a608 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/schedulingenableannotationsupport/SchedulingConfiguration.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorConfiguration.xml new file mode 100644 index 000000000000..96fc1be4e904 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/integration/schedulingtaskexecutorusage/TaskExecutorConfiguration.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.xml new file mode 100644 index 000000000000..5faef120118e --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.xml @@ -0,0 +1,23 @@ + + + + + + + + + + json=application/json + xml=application/xml + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.xml new file mode 100644 index 000000000000..f0bf33102ad5 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.xml new file mode 100644 index 000000000000..da68dad89ac8 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.xml @@ -0,0 +1,14 @@ + + + + + + + \ 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 new file mode 100644 index 000000000000..51f91158b468 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.xml new file mode 100644 index 000000000000..f63d2aab1ff3 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.xml new file mode 100644 index 000000000000..b19e510a5822 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.xml new file mode 100644 index 000000000000..685b2a4be0c8 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.xml new file mode 100644 index 000000000000..24883846eddd --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.xml new file mode 100644 index 000000000000..782b3cadce80 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.xml new file mode 100644 index 000000000000..097ca62890c5 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.xml new file mode 100644 index 000000000000..d019b5533535 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.xml new file mode 100644 index 000000000000..f6dba12f1f1f --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.xml new file mode 100644 index 000000000000..d6ae9519abfc --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.xml new file mode 100644 index 000000000000..a00ad1073ae7 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.xml new file mode 100644 index 000000000000..e3ceacddb01b --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/MessageSizeLimitWebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/MessageSizeLimitWebSocketConfiguration.xml new file mode 100644 index 000000000000..2379035dafbb --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/MessageSizeLimitWebSocketConfiguration.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/WebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/WebSocketConfiguration.xml new file mode 100644 index 000000000000..0926960c9c8a --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompconfigurationperformance/WebSocketConfiguration.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/WebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/WebSocketConfiguration.xml new file mode 100644 index 000000000000..60f3334ff4a4 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompdestinationseparator/WebSocketConfiguration.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompenable/WebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompenable/WebSocketConfiguration.xml new file mode 100644 index 000000000000..910ef94886a9 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstompenable/WebSocketConfiguration.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelay/WebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelay/WebSocketConfiguration.xml new file mode 100644 index 000000000000..0edc2c44994c --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstomphandlebrokerrelay/WebSocketConfiguration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/PublishOrderWebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/PublishOrderWebSocketConfiguration.xml new file mode 100644 index 000000000000..976e6ca573c9 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/stomp/websocketstomporderedmessages/PublishOrderWebSocketConfiguration.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketfallbacksockjsenable/WebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketfallbacksockjsenable/WebSocketConfiguration.xml new file mode 100644 index 000000000000..81ce03b7f491 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketfallbacksockjsenable/WebSocketConfiguration.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverallowedorigins/WebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverallowedorigins/WebSocketConfiguration.xml new file mode 100644 index 000000000000..5736abfcb1f0 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverallowedorigins/WebSocketConfiguration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverhandler/WebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverhandler/WebSocketConfiguration.xml new file mode 100644 index 000000000000..e400f9eb73ee --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverhandler/WebSocketConfiguration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverhandshake/WebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverhandshake/WebSocketConfiguration.xml new file mode 100644 index 000000000000..b7bffe51c774 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverhandshake/WebSocketConfiguration.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/WebSocketConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/WebSocketConfiguration.xml new file mode 100644 index 000000000000..c4e067b264cf --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/WebSocketConfiguration.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index abfad91caaf3..0bd40d427a3e 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -7,146 +7,144 @@ javaPlatform { } dependencies { - api(platform("com.fasterxml.jackson:jackson-bom:2.15.2")) - api(platform("io.micrometer:micrometer-bom:1.12.0-RC1")) - api(platform("io.netty:netty-bom:4.1.100.Final")) - api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2023.0.0-RC1")) - api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.15")) - api(platform("org.apache.logging.log4j:log4j-bom:2.20.0")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.2")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.2")) - api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) - api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0")) - api(platform("org.junit:junit-bom:5.10.0")) - api(platform("org.mockito:mockito-bom:5.6.0")) + api(platform("com.fasterxml.jackson:jackson-bom:2.18.3")) + api(platform("io.micrometer:micrometer-bom:1.14.5")) + api(platform("io.netty:netty-bom:4.1.119.Final")) + api(platform("io.projectreactor:reactor-bom:2025.0.0-M1")) + api(platform("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:3.0.0-beta3")) + api(platform("org.assertj:assertj-bom:3.27.3")) + api(platform("org.eclipse.jetty:jetty-bom:12.1.0.alpha2")) + api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.alpha2")) + api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.1")) + api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.0")) + api(platform("org.junit:junit-bom:5.12.2")) + api(platform("org.mockito:mockito-bom:5.17.0")) constraints { api("com.fasterxml:aalto-xml:1.3.2") - api("com.fasterxml.woodstox:woodstox-core:6.5.1") - api("com.github.ben-manes.caffeine:caffeine:3.1.8") - api("com.github.librepdf:openpdf:1.3.30") + api("com.fasterxml.woodstox:woodstox-core:6.7.0") + api("com.github.ben-manes.caffeine:caffeine:3.2.0") + 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.10.1") - api("com.google.protobuf:protobuf-java-util:3.24.4") - api("com.h2database:h2:2.2.220") - api("com.jayway.jsonpath:json-path:2.8.0") + api("com.google.code.gson:gson:2.12.1") + 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.0.3") - api("com.thoughtworks.xstream:xstream:1.4.20") - api("commons-io:commons-io:2.11.0") + 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.0-RC1") + 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") api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") - api("io.reactivex.rxjava3:rxjava:3.1.6") - api("io.smallrye.reactive:mutiny:1.9.0") - api("io.undertow:undertow-core:2.3.8.Final") - api("io.undertow:undertow-servlet:2.3.8.Final") - api("io.undertow:undertow-websockets-jsr:2.3.8.Final") + api("io.reactivex.rxjava3:rxjava:3.1.10") + api("io.smallrye.reactive:mutiny:1.10.0") + api("io.undertow:undertow-core:2.3.18.Final") + api("io.undertow:undertow-servlet:2.3.18.Final") + 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") api("net.sf.jopt-simple:jopt-simple:5.0.4") - api("net.sourceforge.htmlunit:htmlunit:2.70.0") api("org.apache-extras.beanshell:bsh:2.0b6") - api("org.apache.activemq:activemq-broker:5.17.4") - api("org.apache.activemq:activemq-kahadb-store:5.17.4") - api("org.apache.activemq:activemq-stomp:5.17.4") - api("org.apache.activemq:artemis-junit-5:2.29.0") - api("org.apache.activemq:artemis-jakarta-client:2.29.0") + api("org.apache.activemq:activemq-broker:5.17.6") + api("org.apache.activemq:activemq-kahadb-store:5.17.6") + api("org.apache.activemq:activemq-stomp:5.17.6") + api("org.apache.activemq:artemis-jakarta-client:2.31.2") + api("org.apache.activemq:artemis-junit-5:2.31.2") api("org.apache.commons:commons-pool2:2.9.0") 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.2.1") - api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.2") - api("org.apache.poi:poi-ooxml:5.2.3") - api("org.apache.tomcat.embed:tomcat-embed-core:10.1.14") - api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.14") - api("org.apache.tomcat:tomcat-util:10.1.14") - api("org.apache.tomcat:tomcat-websocket:10.1.14") - api("org.aspectj:aspectjrt:1.9.20.1") - api("org.aspectj:aspectjtools:1.9.20.1") - api("org.aspectj:aspectjweaver:1.9.20.1") - api("org.assertj:assertj-core:3.24.2") - api("org.awaitility:awaitility:4.2.0") + api("org.apache.httpcomponents.client5:httpclient5:5.4.3") + api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4") + api("org.apache.poi:poi-ooxml:5.2.5") + api("org.apache.tomcat.embed:tomcat-embed-core:11.0.5") + api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.5") + api("org.apache.tomcat:tomcat-util:11.0.5") + api("org.apache.tomcat:tomcat-websocket:11.0.5") + api("org.aspectj:aspectjrt:1.9.23") + api("org.aspectj:aspectjtools:1.9.23") + api("org.aspectj:aspectjweaver:1.9.23") + 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.eclipse.jetty:jetty-reactive-httpclient:4.0.0") - api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4") - api("org.eclipse:yasson:2.0.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: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.32") - // Substitute for "javax.management:jmxremote_optional:1.0.1_04" which - // is not available on Maven Central + api("org.freemarker:freemarker:2.3.34") api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea") - api("org.glassfish.tyrus:tyrus-container-servlet:2.1.3") api("org.glassfish:jakarta.el:4.0.2") 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.hsqldb:hsqldb:2.7.2") - api("org.javamoney:moneta:1.4.2") - api("org.jruby:jruby:9.4.3.0") - api("org.junit.support:testng-engine:1.0.4") - api("org.mozilla:rhino:1.7.14") + api("org.hamcrest:hamcrest:3.0") + api("org.hibernate.orm:hibernate-core:7.0.0.Beta5") + api("org.hibernate.validator:hibernate-validator:9.0.0.CR1") + api("org.hsqldb:hsqldb:2.7.4") + api("org.htmlunit:htmlunit:4.10.0") + api("org.javamoney:moneta:1.4.4") + 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.3") + api("org.python:jython-standalone:2.7.4") api("org.quartz-scheduler:quartz:2.3.2") - api("org.seleniumhq.selenium:htmlunit-driver:2.70.0") - api("org.seleniumhq.selenium:selenium-java:3.141.59") - api("org.skyscreamer:jsonassert:1.5.1") - api("org.slf4j:slf4j-api:2.0.9") - api("org.testng:testng:7.8.0") + 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:2.0-rc1") + api("org.testng:testng:7.11.0") api("org.webjars:underscorejs:1.8.3") - api("org.webjars:webjars-locator-core:0.53") - api("org.xmlunit:xmlunit-assertj:2.9.1") - api("org.xmlunit:xmlunit-matchers:2.9.1") - api("org.yaml:snakeyaml:2.0") + 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") + api("org.yaml:snakeyaml:2.4") } } diff --git a/gradle.properties b/gradle.properties index 766b91f0abce..54f2e67903cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,10 @@ -version=6.1.0-SNAPSHOT +version=7.0.0-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m org.gradle.parallel=true -kotlinVersion=1.9.20 +kotlinVersion=2.1.20 kotlin.jvm.target.validation.mode=ignore kotlin.stdlib.default.dependency=false diff --git a/gradle/docs-dokka.gradle b/gradle/docs-dokka.gradle index 147c39497f2a..7d593bf49de1 100644 --- a/gradle/docs-dokka.gradle +++ b/gradle/docs-dokka.gradle @@ -6,6 +6,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure { classpath.from(sourceSets["main"].runtimeClasspath) externalDocumentationLink { url.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/")) + packageListUrl.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/element-list")) } externalDocumentationLink { url.set(new URL("https://projectreactor.io/docs/core/release/api/")) @@ -21,6 +22,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure { } externalDocumentationLink { url.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/")) + packageListUrl.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list")) } externalDocumentationLink { url.set(new URL("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/")) diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 316d7634fcb5..56865ee11b8b 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -69,6 +69,13 @@ eclipse.classpath.file.whenMerged { } } +// Remove Java 21 classpath entries, since we currently use Java 17 +// 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)\/java(21|24)/ } +} + // Remove classpath entries for non-existent libraries added by the me.champeau.mrjar // plugin, such as "spring-core/build/classes/kotlin/java21". eclipse.classpath.file.whenMerged { @@ -77,8 +84,21 @@ eclipse.classpath.file.whenMerged { } } +// Due to an apparent bug in Gradle, even though we exclude the "main" classpath +// entries for sources generated by XJC in spring-oxm.gradle, the Gradle eclipse +// plugin still includes them in the generated .classpath file. So, we have to +// manually remove those lingering "main" entries. +if (project.name == "spring-oxm") { + eclipse.classpath.file.whenMerged { classpath -> + classpath.entries.removeAll { + it.path =~ /build\/generated\/sources\/xjc\/.+/ && + it.entryAttributes.get("gradle_scope") == "main" + } + } +} + // 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', @@ -87,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/publications.gradle b/gradle/publications.gradle index 86e0d2221c0b..db0772caa4f0 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -29,7 +29,7 @@ publishing { developer { id = "jhoeller" name = "Juergen Hoeller" - email = "jhoeller@pivotal.io" + email = "juergen.hoeller@broadcom.com" } } issueManagement { diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 130c00fe6222..7d183af2f8cd 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -3,14 +3,18 @@ apply plugin: 'org.springframework.build.conventions' apply plugin: 'org.springframework.build.optional-dependencies' // Uncomment the following for Shadow support in the jmhJar block. // Currently commented out due to ZipException: archive is not a ZIP archive -// apply plugin: 'com.github.johnrengelman.shadow' +// apply plugin: 'io.github.goooler.shadow' apply plugin: 'me.champeau.jmh' apply from: "$rootDir/gradle/publications.gradle" +apply plugin: 'net.ltgt.errorprone' dependencies { - jmh 'org.openjdk.jmh:jmh-core:1.36' - jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.36' + jmh 'org.openjdk.jmh:jmh-core:1.37' + jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.37' jmh 'net.sf.jopt-simple:jopt-simple' + errorprone 'com.uber.nullaway:nullaway:0.12.4' + errorprone 'com.google.errorprone:error_prone_core:2.36.0' } pluginManager.withPlugin("kotlin") { @@ -72,9 +76,9 @@ javadoc { 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") + options.setOutputLevel(JavadocOutputLevel.QUIET) + // Check for syntax during linting. + options.addBooleanOption("Xdoclint:syntax", true) // Suppress warnings due to cross-module @see and @link references. // Note that global 'api' task does display all warnings, and @@ -83,14 +87,15 @@ javadoc { 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 } @@ -108,3 +113,16 @@ publishing { // Disable publication of test fixture artifacts. components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() } components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() } + +tasks.withType(JavaCompile).configureEach { + options.errorprone { + disableAllChecks = true + option("NullAway:OnlyNullMarked", "true") + option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") + option("NullAway:JSpecifyMode", "true") + } +} +tasks.compileJava { + // The check defaults to a warning, bump it up to an error for the main sources + options.errorprone.error("NullAway") +} diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle deleted file mode 100644 index c432d2735b91..000000000000 --- a/gradle/toolchains.gradle +++ /dev/null @@ -1,98 +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 - // Remove when JDK 22 is supported by Mockito - if (testLanguageVersion == JavaLanguageVersion.of(22)) { - 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.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.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 7f93135c49b7..9bbc975c742b 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 3fa8f862f753..37f853b1c84d 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.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426907..faf93008b77e 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -203,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. diff --git a/gradlew.bat b/gradlew.bat index 6689b85beecd..9b42019c7915 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail 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/config/AopNamespaceHandlerScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java index 683a5d994a06..c711d6e4e75a 100644 --- a/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.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. @@ -75,7 +75,7 @@ void testSingletonScoping() throws Exception { } @Test - void testRequestScoping() throws Exception { + void testRequestScoping() { MockHttpServletRequest oldRequest = new MockHttpServletRequest(); MockHttpServletRequest newRequest = new MockHttpServletRequest(); @@ -103,7 +103,7 @@ void testRequestScoping() throws Exception { } @Test - void testSessionScoping() throws Exception { + void testSessionScoping() { MockHttpSession oldSession = new MockHttpSession(); MockHttpSession newSession = new MockHttpSession(); 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 48142f4c9374..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,11 +16,11 @@ package org.springframework.aop.framework.autoproxy; -import java.io.IOException; import java.lang.reflect.Method; 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; @@ -32,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; @@ -61,12 +60,12 @@ class AdvisorAutoProxyCreatorIntegrationTests { /** * Return a bean factory with attributes and EnterpriseServices configured. */ - protected BeanFactory getBeanFactory() throws IOException { + protected BeanFactory getBeanFactory() { return new ClassPathXmlApplicationContext(DEFAULT_CONTEXT, CLASS); } @Test - void testDefaultExclusionPrefix() throws Exception { + void testDefaultExclusionPrefix() { DefaultAdvisorAutoProxyCreator aapc = (DefaultAdvisorAutoProxyCreator) getBeanFactory().getBean(ADVISOR_APC_BEAN_NAME); assertThat(aapc.getAdvisorBeanNamePrefix()).isEqualTo((ADVISOR_APC_BEAN_NAME + DefaultAdvisorAutoProxyCreator.SEPARATOR)); assertThat(aapc.isUsePrefix()).isFalse(); @@ -76,21 +75,21 @@ void testDefaultExclusionPrefix() throws Exception { * If no pointcuts match (no attrs) there should be proxying. */ @Test - void testNoProxy() throws Exception { + void testNoProxy() { BeanFactory bf = getBeanFactory(); Object o = bf.getBean("noSetters"); assertThat(AopUtils.isAopProxy(o)).isFalse(); } @Test - void testTxIsProxied() throws Exception { + void testTxIsProxied() { BeanFactory bf = getBeanFactory(); ITestBean test = (ITestBean) bf.getBean("test"); assertThat(AopUtils.isAopProxy(test)).isTrue(); } @Test - void testRegexpApplied() throws Exception { + void testRegexpApplied() { BeanFactory bf = getBeanFactory(); ITestBean test = (ITestBean) bf.getBean("test"); MethodCounter counter = (MethodCounter) bf.getBean("countingAdvice"); @@ -100,7 +99,7 @@ void testRegexpApplied() throws Exception { } @Test - void testTransactionAttributeOnMethod() throws Exception { + void testTransactionAttributeOnMethod() { BeanFactory bf = getBeanFactory(); ITestBean test = (ITestBean) bf.getBean("test"); @@ -166,7 +165,7 @@ void testRollbackRulesOnMethodPreventRollback() throws Exception { } @Test - void testProgrammaticRollback() throws Exception { + void testProgrammaticRollback() { BeanFactory bf = getBeanFactory(); Object bean = bf.getBean(TXMANAGER_BEAN_NAME); @@ -250,7 +249,7 @@ public CountingBeforeAdvice getCountingBeforeAdvice() { } @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { setAdvice(new TxCountingBeforeAdvice()); } diff --git a/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java index 2309cbc01d6e..1de66c6612a9 100644 --- a/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java +++ b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.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. @@ -46,7 +46,7 @@ * @author Brian Clozel */ @EnabledIfRuntimeHintsAgent -public class RuntimeHintsAgentTests { +class RuntimeHintsAgentTests { private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); @@ -58,7 +58,7 @@ public class RuntimeHintsAgentTests { @BeforeAll - public static void classSetup() throws NoSuchMethodException { + static void classSetup() throws NoSuchMethodException { defaultConstructor = String.class.getConstructor(); toStringMethod = String.class.getMethod("toString"); privateGreetMethod = PrivateClass.class.getDeclaredMethod("greet"); 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/beans/factory/xml/ComponentBeanDefinitionParserTests.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java index 31eee877ed5e..613fcb32e9d2 100644 --- a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ class ComponentBeanDefinitionParserTests { @BeforeAll - void setUp() throws Exception { + void setUp() { new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("component-config.xml", ComponentBeanDefinitionParserTests.class)); } diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java index ec2479cd196f..12255e838860 100644 --- a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -34,7 +34,7 @@ public void setChildren(List children) { } @Override - public Component getObject() throws Exception { + public Component getObject() { if (this.children != null && this.children.size() > 0) { for (Component child : children) { this.parent.addComponent(child); diff --git a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java index 41acb6ed5d62..104ca11ca1e4 100644 --- a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.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. @@ -42,7 +42,6 @@ * @author Chris Beams * @since 3.1 */ -@SuppressWarnings("resource") class EnableCachingIntegrationTests { @Test @@ -63,7 +62,7 @@ void repositoryUsesAspectJAdviceMode() { // attempt was made to look up the AJ aspect. It's due to classpath issues // in integration-tests that it's not found. assertThatException().isThrownBy(ctx::refresh) - .withMessageContaining("AspectJCachingConfiguration"); + .withMessageContaining("AspectJCachingConfiguration"); } diff --git a/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java index 2bbc001f28eb..824099b4c2c6 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -87,7 +87,6 @@ * @author Sam Brannen * @see org.springframework.context.support.EnvironmentIntegrationTests */ -@SuppressWarnings("resource") public class EnvironmentSystemIntegrationTests { private final ConfigurableEnvironment prodEnv = new StandardEnvironment(); @@ -542,8 +541,7 @@ void abstractApplicationContextValidatesRequiredPropertiesOnRefresh() { { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setRequiredProperties("foo", "bar"); - assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( - ctx::refresh); + assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(ctx::refresh); } { @@ -618,7 +616,7 @@ public void setEnvironment(Environment environment) { @Import({DevConfig.class, ProdConfig.class}) static class Config { @Bean - public EnvironmentAwareBean envAwareBean() { + EnvironmentAwareBean envAwareBean() { return new EnvironmentAwareBean(); } } diff --git a/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java b/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java index 63174d2d3327..59da59d7ef58 100644 --- a/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java +++ b/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.TypeConverter; +import org.springframework.util.ClassUtils; /** * Copied from Spring Integration for purposes of reproducing @@ -59,11 +60,9 @@ public void setConversionService(ConversionService conversionService) { @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - if (beanFactory instanceof ConfigurableBeanFactory) { - Object typeConverter = ((ConfigurableBeanFactory) beanFactory).getTypeConverter(); - if (typeConverter instanceof SimpleTypeConverter) { - delegate = (SimpleTypeConverter) typeConverter; - } + if (beanFactory instanceof ConfigurableBeanFactory cbf && + cbf.getTypeConverter() instanceof SimpleTypeConverter simpleTypeConverter) { + this.delegate = simpleTypeConverter; } } @@ -86,7 +85,6 @@ public boolean canConvert(TypeDescriptor sourceTypeDescriptor, TypeDescriptor ta if (conversionService.canConvert(sourceTypeDescriptor, targetTypeDescriptor)) { return true; } - // TODO: what does this mean? This method is not used in SpEL so probably ignorable? Class sourceType = sourceTypeDescriptor.getObjectType(); Class targetType = targetTypeDescriptor.getObjectType(); return canConvert(sourceType, targetType); @@ -94,7 +92,7 @@ public boolean canConvert(TypeDescriptor sourceTypeDescriptor, TypeDescriptor ta @Override public Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (targetType.getType() == Void.class || targetType.getType() == Void.TYPE) { + if (ClassUtils.isVoidType(targetType.getType())) { return null; } if (conversionService.canConvert(sourceType, targetType)) { diff --git a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java index b6f5102491a6..050e3793f7f2 100644 --- a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.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. @@ -51,7 +51,6 @@ * @author Juergen Hoeller * @since 3.1 */ -@SuppressWarnings("resource") @EnabledForTestGroups(LONG_RUNNING) class ScheduledAndTransactionalAnnotationIntegrationTests { diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java index 4ab8cab1569e..9b7eef5a0961 100644 --- a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.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. @@ -54,7 +54,6 @@ * @author Sam Brannen * @since 3.1 */ -@SuppressWarnings("resource") class EnableTransactionManagementIntegrationTests { @Test @@ -98,9 +97,8 @@ void repositoryUsesAspectJAdviceMode() { // this test is a bit fragile, but gets the job done, proving that an // attempt was made to look up the AJ aspect. It's due to classpath issues // in integration-tests that it's not found. - assertThatException() - .isThrownBy(ctx::refresh) - .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); + assertThatException().isThrownBy(ctx::refresh) + .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); } @Test diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java index 67a61e85fab8..510fc08d92f4 100644 --- a/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java +++ b/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,14 +27,13 @@ /** * Tests proving that regardless the proxy strategy used (JDK interface-based vs. CGLIB * subclass-based), discovery of advice-oriented annotations is consistent. - * + *

* For example, Spring's @Transactional may be declared at the interface or class level, * and whether interface or subclass proxies are used, the @Transactional annotation must * be discovered in a consistent fashion. * * @author Chris Beams */ -@SuppressWarnings("resource") class ProxyAnnotationDiscoveryTests { @Test diff --git a/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt b/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt index c8f1bdea2643..7cbd35532bd7 100644 --- a/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt +++ b/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt @@ -1,21 +1,52 @@ +/* + * 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.aop.framework.autoproxy import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.aopalliance.intercept.MethodInterceptor import org.aopalliance.intercept.MethodInvocation +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.aop.framework.autoproxy.AspectJAutoProxyInterceptorKotlinIntegrationTests.InterceptorConfig import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor import org.springframework.beans.factory.annotation.Autowired +import org.springframework.cache.CacheManager +import org.springframework.cache.annotation.Cacheable +import org.springframework.cache.annotation.EnableCaching +import org.springframework.cache.concurrent.ConcurrentMapCacheManager import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.EnableAspectJAutoProxy +import org.springframework.stereotype.Component import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.junit.jupiter.SpringJUnitConfig +import org.springframework.transaction.annotation.EnableTransactionManagement +import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.testfixture.ReactiveCallCountingTransactionManager import reactor.core.publisher.Mono import java.lang.reflect.Method +import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.TYPE /** @@ -25,83 +56,162 @@ import java.lang.reflect.Method @SpringJUnitConfig(InterceptorConfig::class) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class AspectJAutoProxyInterceptorKotlinIntegrationTests( - @Autowired val echo: Echo, - @Autowired val firstAdvisor: TestPointcutAdvisor, - @Autowired val secondAdvisor: TestPointcutAdvisor) { - - @Test - fun `Multiple interceptors with regular function`() { - assertThat(firstAdvisor.interceptor.invocations).isEmpty() - assertThat(secondAdvisor.interceptor.invocations).isEmpty() - val value = "Hello!" - assertThat(echo.echo(value)).isEqualTo(value) + @Autowired val echo: Echo, + @Autowired val firstAdvisor: TestPointcutAdvisor, + @Autowired val secondAdvisor: TestPointcutAdvisor, + @Autowired val countingAspect: CountingAspect, + @Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) { + + @Test + fun `Multiple interceptors with regular function`() { + assertThat(firstAdvisor.interceptor.invocations).isEmpty() + assertThat(secondAdvisor.interceptor.invocations).isEmpty() + val value = "Hello!" + assertThat(echo.echo(value)).isEqualTo(value) assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) } assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) } - } - - @Test - fun `Multiple interceptors with suspending function`() { - assertThat(firstAdvisor.interceptor.invocations).isEmpty() - assertThat(secondAdvisor.interceptor.invocations).isEmpty() - val value = "Hello!" - runBlocking { - assertThat(echo.suspendingEcho(value)).isEqualTo(value) - } + } + + @Test + fun `Multiple interceptors with suspending function`() { + assertThat(firstAdvisor.interceptor.invocations).isEmpty() + assertThat(secondAdvisor.interceptor.invocations).isEmpty() + val value = "Hello!" + runBlocking { + assertThat(echo.suspendingEcho(value)).isEqualTo(value) + } assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) } assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) } - } - - @Configuration - @EnableAspectJAutoProxy - open class InterceptorConfig { - - @Bean - open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 } - - @Bean - open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 } - - - @Bean - open fun echo(): Echo { - return Echo() - } - } - - class TestMethodInterceptor: MethodInterceptor { - - var invocations: MutableList> = mutableListOf() - - @Suppress("RedundantNullableReturnType") - override fun invoke(invocation: MethodInvocation): Any? { - val result = invocation.proceed() - invocations.add(result!!.javaClass) - return result - } - - } - - class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) { - - val interceptor: TestMethodInterceptor - get() = advice as TestMethodInterceptor - - override fun matches(method: Method, targetClass: Class<*>): Boolean { - return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo") - } - } - - open class Echo { - - open fun echo(value: String): String { - return value; - } - - open suspend fun suspendingEcho(value: String): String { - delay(1) - return value; - } + } + + @Test // gh-33095 + fun `Aspect and reactive transactional with suspending function`() { + assertThat(countingAspect.counter).isZero() + assertThat(reactiveTransactionManager.commits).isZero() + val value = "Hello!" + runBlocking { + assertThat(echo.suspendingTransactionalEcho(value)).isEqualTo(value) + } + assertThat(countingAspect.counter).`as`("aspect applied").isOne() + assertThat(reactiveTransactionManager.begun).isOne() + assertThat(reactiveTransactionManager.commits).`as`("transactional applied").isOne() + } + + @Test // gh-33210 + fun `Aspect and cacheable with suspending function`() { + assertThat(countingAspect.counter).isZero() + val value = "Hello!" + runBlocking { + assertThat(echo.suspendingCacheableEcho(value)).isEqualTo("$value 0") + assertThat(echo.suspendingCacheableEcho(value)).isEqualTo("$value 0") + assertThat(echo.suspendingCacheableEcho(value)).isEqualTo("$value 0") + assertThat(countingAspect.counter).`as`("aspect applied once").isOne() + + assertThat(echo.suspendingCacheableEcho("$value bis")).isEqualTo("$value bis 1") + assertThat(echo.suspendingCacheableEcho("$value bis")).isEqualTo("$value bis 1") + } + assertThat(countingAspect.counter).`as`("aspect applied once per key").isEqualTo(2) + } + + @Configuration + @EnableAspectJAutoProxy + @EnableTransactionManagement + @EnableCaching + open class InterceptorConfig { + + @Bean + open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 } + + @Bean + open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 } + + @Bean + open fun countingAspect() = CountingAspect() + + @Bean + open fun transactionManager(): ReactiveCallCountingTransactionManager { + return ReactiveCallCountingTransactionManager() + } + + @Bean + open fun cacheManager(): CacheManager { + return ConcurrentMapCacheManager() + } + + @Bean + open fun echo(): Echo { + return Echo() + } + } + + class TestMethodInterceptor: MethodInterceptor { + + var invocations: MutableList> = mutableListOf() + + @Suppress("RedundantNullableReturnType") + override fun invoke(invocation: MethodInvocation): Any? { + val result = invocation.proceed() + invocations.add(result!!.javaClass) + return result + } + + } + + class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) { + + val interceptor: TestMethodInterceptor + get() = advice as TestMethodInterceptor + + override fun matches(method: Method, targetClass: Class<*>): Boolean { + return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo") + } + } + + @Target(CLASS, FUNCTION, ANNOTATION_CLASS, TYPE) + @Retention(AnnotationRetention.RUNTIME) + annotation class Counting() + + @Aspect + @Component + class CountingAspect { + + var counter: Long = 0 + + @Around("@annotation(org.springframework.aop.framework.autoproxy.AspectJAutoProxyInterceptorKotlinIntegrationTests.Counting)") + fun logging(joinPoint: ProceedingJoinPoint): Any { + return (joinPoint.proceed(joinPoint.args) as Mono<*>).doOnTerminate { + counter++ + }.checkpoint("CountingAspect") + } + } + + open class Echo { + + open fun echo(value: String): String { + return value + } + + open suspend fun suspendingEcho(value: String): String { + delay(1) + return value + } + + @Transactional + @Counting + open suspend fun suspendingTransactionalEcho(value: String): String { + delay(1) + return value + } + + open var cacheCounter: Int = 0 + + @Counting + @Cacheable("something") + open suspend fun suspendingCacheableEcho(value: String): String { + delay(1) + return "$value ${cacheCounter++}" + } - } + } } diff --git a/settings.gradle b/settings.gradle index 8f9f81e27c8d..e9fe33a3143a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,6 @@ plugins { - id "com.gradle.enterprise" version "3.14" - id "io.spring.ge.conventions" version "0.0.13" - id "org.gradle.toolchains.foojay-resolver-convention" version "0.7.0" + id "io.spring.develocity.conventions" version "0.0.22" + id "org.gradle.toolchains.foojay-resolver-convention" version "0.9.0" } include "spring-aop" @@ -14,7 +13,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" @@ -39,7 +37,7 @@ rootProject.children.each {project -> } settings.gradle.projectsLoaded { - gradleEnterprise { + develocity { buildScan { File buildDir = settings.gradle.rootProject .getLayout().getBuildDirectory().getAsFile().get() 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 08b02a502fa2..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,14 +16,12 @@ package org.aopalliance.intercept; -import javax.annotation.Nonnull; - /** * Intercepts the construction of a new object. * *

The user should implement the {@link * #construct(ConstructorInvocation)} method to modify the original - * behavior. E.g. the following class implements a singleton + * behavior. For example, the following class implements a singleton * interceptor (allows only one unique instance for the intercepted * class): * @@ -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 9188e25e1d0d..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,15 +16,14 @@ 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 * are nested "on top" of the target. * *

The user should implement the {@link #invoke(MethodInvocation)} - * method to modify the original behavior. E.g. the following class + * method to modify the original behavior. For example, the following class * implements a tracing interceptor (traces all the calls on the * intercepted method(s)): * @@ -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/Pointcut.java b/spring-aop/src/main/java/org/springframework/aop/Pointcut.java index ffcf92ef316c..fc860df1c360 100644 --- a/spring-aop/src/main/java/org/springframework/aop/Pointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/Pointcut.java @@ -21,7 +21,7 @@ * *

A pointcut is composed of a {@link ClassFilter} and a {@link MethodMatcher}. * Both these basic terms and a Pointcut itself can be combined to build up combinations - * (e.g. through {@link org.springframework.aop.support.ComposablePointcut}). + * (for example, through {@link org.springframework.aop.support.ComposablePointcut}). * * @author Rod Johnson * @see ClassFilter 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 e894c5cb0a10..9429d43bb7bc 100644 --- a/spring-aop/src/main/java/org/springframework/aop/TargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/TargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -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,17 +42,19 @@ 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? *

In that case, there will be no need to invoke {@link #releaseTarget(Object)}, * and the AOP framework can cache the return value of {@link #getTarget()}. + *

The default implementation returns {@code false}. * @return {@code true} if the target is immutable * @see #getTarget */ - boolean isStatic(); + default boolean isStatic() { + return false; + } /** * Return a target instance. Invoked immediately before the @@ -61,15 +63,16 @@ public interface TargetSource extends TargetClassAware { * 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 * {@link #getTarget()} method, if any. + *

The default implementation is empty. * @param target object obtained from a call to {@link #getTarget()} * @throws Exception if the object can't be released */ - void releaseTarget(Object target) throws Exception; + default void releaseTarget(Object target) throws Exception { + } } diff --git a/spring-aop/src/main/java/org/springframework/aop/ThrowsAdvice.java b/spring-aop/src/main/java/org/springframework/aop/ThrowsAdvice.java index ef50fe8b8263..f6e453b40e3a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/ThrowsAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/ThrowsAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 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,11 +27,11 @@ *

Some examples of valid methods would be: * *

public void afterThrowing(Exception ex)
- *
public void afterThrowing(RemoteException)
+ *
public void afterThrowing(RemoteException ex)
*
public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
*
public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)
* - * The first three arguments are optional, and only useful if we want further + *

The first three arguments are optional, and only useful if we want further * information about the joinpoint, as in AspectJ after-throwing advice. * *

Note: If a throws-advice method throws an exception itself, it will 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 bbe397880b10..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import org.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 '" + @@ -276,14 +272,18 @@ public void setArgumentNamesFromStringArray(String... argumentNames) { } if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) { // May need to add implicit join point arg name... - Class firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0]; - if (firstArgType == JoinPoint.class || - firstArgType == ProceedingJoinPoint.class || - firstArgType == JoinPoint.StaticPart.class) { - String[] oldNames = this.argumentNames; + for (int i = 0; i < this.aspectJAdviceMethod.getParameterCount(); i++) { + Class argType = this.aspectJAdviceMethod.getParameterTypes()[i]; + if (argType == JoinPoint.class || + argType == ProceedingJoinPoint.class || + argType == JoinPoint.StaticPart.class) { + @Nullable String[] oldNames = this.argumentNames; this.argumentNames = new String[oldNames.length + 1]; - this.argumentNames[0] = "THIS_JOIN_POINT"; - System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length); + 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; + } } } } @@ -318,8 +318,7 @@ protected Class getDiscoveredReturningType() { return this.discoveredReturningType; } - @Nullable - protected Type getDiscoveredReturningGenericType() { + protected @Nullable Type getDiscoveredReturningGenericType() { return this.discoveredReturningGenericType; } @@ -353,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); } @@ -463,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<>(); @@ -552,13 +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 */ - 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) { @@ -577,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++; } @@ -584,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++; } @@ -627,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; } @@ -656,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); @@ -671,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 1b33b1122588..b9c657af0d55 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-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,14 +16,11 @@ package org.springframework.aop.aspectj; -import java.io.IOException; -import java.io.ObjectInputStream; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; -import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; @@ -41,6 +38,8 @@ import org.aspectj.weaver.tools.PointcutParser; 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; @@ -56,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; @@ -78,12 +76,15 @@ * @author Juergen Hoeller * @author Ramnivas Laddad * @author Dave Syer + * @author Yanming Zhou * @since 2.0 */ @SuppressWarnings("serial") public class AspectJExpressionPointcut extends AbstractExpressionPointcut implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware { + private static final String AJC_MAGIC = "ajc$"; + private static final Set SUPPORTED_PRIMITIVES = Set.of( PointcutPrimitive.EXECUTION, PointcutPrimitive.ARGS, @@ -98,23 +99,21 @@ 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; private String[] pointcutParameterNames = new String[0]; private Class[] pointcutParameterTypes = new Class[0]; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private transient ClassLoader pointcutClassLoader; + private transient @Nullable ClassLoader pointcutClassLoader; - @Nullable - private transient PointcutExpression pointcutExpression; + private transient @Nullable PointcutExpression pointcutExpression; - private transient Map shadowMatchCache = new ConcurrentHashMap<>(32); + private transient boolean pointcutParsingFailed = false; /** @@ -130,7 +129,7 @@ public AspectJExpressionPointcut() { * @param paramTypes the parameter types for the pointcut */ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, Class[] paramTypes) { - this.pointcutDeclarationScope = declarationScope; + setPointcutDeclarationScope(declarationScope); if (paramNames.length != paramTypes.length) { throw new IllegalStateException( "Number of pointcut parameter names must match number of pointcut parameter types"); @@ -145,6 +144,7 @@ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, */ public void setPointcutDeclarationScope(Class pointcutDeclarationScope) { this.pointcutDeclarationScope = pointcutDeclarationScope; + this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope); } /** @@ -169,25 +169,30 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override public ClassFilter getClassFilter() { - obtainPointcutExpression(); + checkExpression(); return this; } @Override public MethodMatcher getMethodMatcher() { - obtainPointcutExpression(); + checkExpression(); return this; } /** - * Check whether this pointcut is ready to match, - * lazily building the underlying AspectJ pointcut expression. + * Check whether this pointcut is ready to match. */ - private PointcutExpression obtainPointcutExpression() { + private void checkExpression() { if (getExpression() == null) { throw new IllegalStateException("Must set property 'expression' before attempting to match"); } + } + + /** + * Lazily build the underlying AspectJ pointcut expression. + */ + private PointcutExpression obtainPointcutExpression() { if (this.pointcutExpression == null) { this.pointcutClassLoader = determinePointcutClassLoader(); this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); @@ -198,8 +203,7 @@ private PointcutExpression obtainPointcutExpression() { /** * 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(); } @@ -264,10 +268,18 @@ public PointcutExpression getPointcutExpression() { @Override public boolean matches(Class targetClass) { - PointcutExpression pointcutExpression = obtainPointcutExpression(); + if (this.pointcutParsingFailed) { + // Pointcut parsing failed before below -> avoid trying again. + return false; + } + if (this.aspectCompiledByAjc && compiledByAjc(targetClass)) { + // ajc-compiled aspect class for ajc-compiled target class -> already weaved. + return false; + } + try { try { - return pointcutExpression.couldMatchJoinPointsInType(targetClass); + return obtainPointcutExpression().couldMatchJoinPointsInType(targetClass); } catch (ReflectionWorldException ex) { logger.debug("PointcutExpression matching rejected target class - trying fallback expression", ex); @@ -278,6 +290,12 @@ public boolean matches(Class targetClass) { } } } + catch (IllegalArgumentException | IllegalStateException | UnsupportedPointcutPrimitiveException ex) { + this.pointcutParsingFailed = true; + if (logger.isDebugEnabled()) { + logger.debug("Pointcut parser rejected expression [" + getExpression() + "]: " + ex); + } + } catch (Throwable ex) { logger.debug("PointcutExpression matching rejected target class", ex); } @@ -286,7 +304,6 @@ public boolean matches(Class targetClass) { @Override public boolean matches(Method method, Class targetClass, boolean hasIntroductions) { - obtainPointcutExpression(); ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Special handling for this, target, @this, @target, @annotation @@ -323,8 +340,7 @@ public boolean isRuntime() { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { - obtainPointcutExpression(); + 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, @@ -333,13 +349,15 @@ public boolean matches(Method method, Class targetClass, Object... args) { Object targetObject = null; Object thisObject = null; try { - MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation(); - targetObject = mi.getThis(); - if (!(mi instanceof ProxyMethodInvocation _pmi)) { - throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + MethodInvocation curr = ExposeInvocationInterceptor.currentInvocation(); + if (curr.getMethod() == method) { + targetObject = curr.getThis(); + if (!(curr instanceof ProxyMethodInvocation currPmi)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + curr); + } + pmi = currPmi; + thisObject = pmi.getProxy(); } - pmi = _pmi; - thisObject = pmi.getProxy(); } catch (IllegalStateException ex) { // No current invocation... @@ -380,8 +398,7 @@ public boolean matches(Method method, Class targetClass, Object... args) { } } - @Nullable - protected String getCurrentProxiedBeanName() { + protected @Nullable String getCurrentProxiedBeanName() { return ProxyCreationContext.getCurrentProxiedBeanName(); } @@ -389,8 +406,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) { @@ -444,75 +460,83 @@ private ShadowMatch getTargetShadowMatch(Method method, Class targetClass) { } private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { - // Avoid lock contention for known Methods through concurrent access... - ShadowMatch shadowMatch = this.shadowMatchCache.get(targetMethod); + ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(this, targetMethod); if (shadowMatch == null) { - synchronized (this.shadowMatchCache) { - // Not found - now check again with full lock... - PointcutExpression fallbackExpression = null; - shadowMatch = this.shadowMatchCache.get(targetMethod); - if (shadowMatch == null) { - Method methodToMatch = targetMethod; + 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 { - try { - shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); - } - catch (ReflectionWorldException ex) { - // Failed to introspect target method, probably because it has been loaded - // in a special ClassLoader. Let's try the declaring ClassLoader instead... - try { - fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); - if (fallbackExpression != null) { - shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); - } - } - catch (ReflectionWorldException ex2) { - fallbackExpression = null; - } - } - if (targetMethod != originalMethod && (shadowMatch == null || - (shadowMatch.neverMatches() && Proxy.isProxyClass(targetMethod.getDeclaringClass())))) { - // 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). - methodToMatch = originalMethod; - try { - shadowMatch = obtainPointcutExpression().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; - } - } + fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); + if (fallbackExpression != null) { + shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); } } - catch (Throwable ex) { - // Possibly AspectJ 1.8.10 encountering an invalid signature - logger.debug("PointcutExpression matching rejected target method", ex); + catch (ReflectionWorldException ex2) { fallbackExpression = null; } - if (shadowMatch == null) { - shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null); + } + if (targetMethod != originalMethod && (shadowMatch == null || + (Proxy.isProxyClass(targetMethod.getDeclaringClass()) && + (shadowMatch.neverMatches() || containsAnnotationPointcut())))) { + // Fall back to the plain original method in case of no resolvable match or a + // negative match on a proxy class (which doesn't carry any annotations on its + // redeclared methods), as well as for annotation pointcuts. + methodToMatch = originalMethod; + try { + shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); } - else if (shadowMatch.maybeMatches() && fallbackExpression != null) { - shadowMatch = new DefensiveShadowMatch(shadowMatch, - fallbackExpression.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; + } } - this.shadowMatchCache.put(targetMethod, 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; } + private boolean containsAnnotationPointcut() { + return resolveExpression().contains("@annotation"); + } + + private static boolean compiledByAjc(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().startsWith(AJC_MAGIC)) { + return true; + } + } + Class superclass = clazz.getSuperclass(); + return (superclass != null && compiledByAjc(superclass)); + } + @Override public boolean equals(@Nullable Object other) { @@ -550,19 +574,6 @@ public String toString() { return sb.toString(); } - //--------------------------------------------------------------------- - // Serialization support - //--------------------------------------------------------------------- - - private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { - // Rely on default serialization, just initialize state after deserialization. - ois.defaultReadObject(); - - // Initialize transient fields. - // pointcutExpression will be initialized lazily by checkReadyToMatch() - this.shadowMatchCache = new ConcurrentHashMap<>(32); - } - /** * Handler for the Spring-specific {@code bean()} pointcut designator 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..0b2eb35584c4 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; 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 new file mode 100644 index 000000000000..3c26b4abc52c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java @@ -0,0 +1,74 @@ +/* + * 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.aop.aspectj; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.aspectj.weaver.tools.ShadowMatch; +import org.jspecify.annotations.Nullable; + +import org.springframework.aop.support.ExpressionPointcut; + +/** + * Internal {@link ShadowMatch} utilities. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public abstract class ShadowMatchUtils { + + 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 + */ + static @Nullable ShadowMatch getShadowMatch(ExpressionPointcut expression, Method method) { + return shadowMatchCache.get(new Key(expression, method)); + } + + /** + * Associate the {@link ShadowMatch} to the specified {@link ExpressionPointcut} + * and method. If an entry already exists, the given {@code shadowMatch} is + * ignored. + * @param expression the expression + * @param method the method + * @param shadowMatch the shadow match to use for this expression and method + * if none already exists + * @return the shadow match to use for the specified expression and method + */ + static ShadowMatch setShadowMatch(ExpressionPointcut expression, Method method, ShadowMatch shadowMatch) { + ShadowMatch existing = shadowMatchCache.putIfAbsent(new Key(expression, method), shadowMatch); + return (existing != null ? existing : shadowMatch); + } + + + private record Key(ExpressionPointcut expression, Method method) {} + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/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 bf2eb3e45056..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 @@ -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. @@ -35,11 +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 @@ -56,11 +57,26 @@ */ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory { - private static final String AJC_MAGIC = "ajc$"; - private static final Class[] ASPECTJ_ANNOTATION_CLASSES = new Class[] { Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class}; + private static final String AJC_MAGIC = "ajc$"; + + /** + * System property that instructs Spring to ignore ajc-compiled aspects + * for Spring AOP proxying, restoring traditional Spring behavior for + * scenarios where both weaving and AspectJ auto-proxying are enabled. + *

The default is "false". Consider switching this to "true" if you + * encounter double execution of your aspects in a given build setup. + * Note that we recommend restructuring your AspectJ configuration to + * avoid such double exposure of an AspectJ aspect to begin with. + * @since 6.1.15 + */ + public static final String IGNORE_AJC_PROPERTY_NAME = "spring.aop.ajc.ignore"; + + private static final boolean shouldIgnoreAjcCompiledAspects = + SpringProperties.getFlag(IGNORE_AJC_PROPERTY_NAME); + /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); @@ -68,35 +84,10 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac protected final ParameterNameDiscoverer parameterNameDiscoverer = new AspectJAnnotationParameterNameDiscoverer(); - /** - * We consider something to be an AspectJ aspect suitable for use by the Spring AOP system - * if it has the @Aspect annotation, and was not compiled by ajc. The reason for this latter test - * is that aspects written in the code-style (AspectJ language) also have the annotation present - * when compiled by ajc with the -1.5 flag, yet they cannot be consumed by Spring AOP. - */ @Override public boolean isAspect(Class clazz) { - return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz)); - } - - private boolean hasAspectAnnotation(Class clazz) { - return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null); - } - - /** - * We need to detect this as "code-style" AspectJ aspects should not be - * interpreted by Spring AOP. - */ - private boolean compiledByAjc(Class clazz) { - // The AJTypeSystem goes to great lengths to provide a uniform appearance between code-style and - // annotation-style aspects. Therefore there is no 'clean' way to tell them apart. Here we rely on - // an implementation detail of the AspectJ compiler. - for (Field field : clazz.getDeclaredFields()) { - if (field.getName().startsWith(AJC_MAGIC)) { - return true; - } - } - return false; + return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null && + (!shouldIgnoreAjcCompiledAspects || !compiledByAjc(clazz))); } @Override @@ -115,13 +106,13 @@ public void validate(Class aspectClass) throws AopConfigException { } } + /** * Find and return the first AspectJ annotation on the given method * (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) { @@ -131,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); @@ -142,6 +132,15 @@ private static AspectJAnnotation findAnnotation(Method method, Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().startsWith(AJC_MAGIC)) { + return true; + } + } + return false; + } + /** * Enum for AspectJ annotation types. @@ -241,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; } @@ -265,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 45ea4983644c..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; /** @@ -103,7 +101,7 @@ protected boolean isInfrastructureClass(Class beanClass) { // broad an impact. Instead we now override isInfrastructureClass to avoid proxying // aspects. I'm not entirely happy with that as there is no good reason not // to advise aspects, except that it causes advice invocation to go through a - // proxy, and if the aspect implements e.g the Ordered interface it will be + // proxy, and if the aspect implements, for example, the Ordered interface it will be // proxied by that interface and fail at runtime as the advice method is not // defined on the interface. We could potentially relax the restriction about // not advising aspects in the future. 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 new file mode 100644 index 000000000000..6fa2b9105144 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java @@ -0,0 +1,81 @@ +/* + * 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.aop.aspectj.annotation; + +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.util.ClassUtils; + +/** + * An AOT {@link BeanRegistrationAotProcessor} that detects the presence of + * classes compiled with AspectJ and adds the related required field hints. + * + * @author Sebastien Deleuze + * @since 6.1 + */ +class AspectJAdvisorBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + private static final String AJC_MAGIC = "ajc$"; + + private static final boolean aspectjPresent = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", + AspectJAdvisorBeanRegistrationAotProcessor.class.getClassLoader()); + + + @Override + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + if (aspectjPresent) { + Class beanClass = registeredBean.getBeanClass(); + if (compiledByAjc(beanClass)) { + return new AspectJAdvisorContribution(beanClass); + } + } + return null; + } + + private static boolean compiledByAjc(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().startsWith(AJC_MAGIC)) { + return true; + } + } + return false; + } + + + private static class AspectJAdvisorContribution implements BeanRegistrationAotContribution { + + private final Class beanClass; + + public AspectJAdvisorContribution(Class beanClass) { + this.beanClass = beanClass; + } + + @Override + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + 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 a851fb27b64a..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; /** @@ -40,30 +41,29 @@ */ class AspectJBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { - private static final boolean aspectJPresent = ClassUtils.isPresent( - "org.aspectj.lang.annotation.Pointcut", AspectJBeanFactoryInitializationAotProcessor.class.getClassLoader()); + private static final boolean aspectJPresent = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", + AspectJBeanFactoryInitializationAotProcessor.class.getClassLoader()); + - @Nullable @Override - public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + public @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { if (aspectJPresent) { return AspectDelegate.processAheadOfTime(beanFactory); } return null; } + /** * Inner class to avoid a hard dependency on AspectJ at runtime. */ 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)); } - } @@ -84,7 +84,6 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati } } } - } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java index 969ec866eebb..2775bc9fd32c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.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. @@ -124,10 +124,16 @@ public AspectMetadata(Class aspectClass, String aspectName) { * Extract contents from String of form {@code pertarget(contents)}. */ private String findPerClause(Class aspectClass) { - String str = aspectClass.getAnnotation(Aspect.class).value(); - int beginIndex = str.indexOf('(') + 1; - int endIndex = str.length() - 1; - return str.substring(beginIndex, endIndex); + Aspect ann = aspectClass.getAnnotation(Aspect.class); + if (ann == null) { + return ""; + } + String value = ann.value(); + int beginIndex = value.indexOf('('); + if (beginIndex < 0) { + return ""; + } + return value.substring(beginIndex + 1, value.length() - 1); } @@ -154,7 +160,7 @@ public String getAspectName() { /** * Return a Spring pointcut expression for a singleton aspect. - * (e.g. {@code Pointcut.TRUE} if it's a singleton). + * (for example, {@code Pointcut.TRUE} if it's a singleton). */ public Pointcut getPerClausePointcut() { return this.perClausePointcut; 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 3bdbb9c16abd..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 @@ -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,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; @@ -77,7 +78,7 @@ public BeanFactoryAspectInstanceFactory(BeanFactory beanFactory, String name, @N this.beanFactory = beanFactory; this.name = name; Class resolvedType = type; - if (type == null) { + if (resolvedType == null) { resolvedType = beanFactory.getType(name); Assert.notNull(resolvedType, "Unresolvable bean type - explicitly specify the aspect class"); } @@ -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,19 +103,13 @@ 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; } - else if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { - // No singleton guarantees from the factory -> let's lock locally but - // reuse the factory's singleton lock, just in case a lazy dependency - // of our advice bean happens to trigger the singleton lock implicitly... - return cbf.getSingletonMutex(); - } else { + // No singleton guarantees from the factory -> let's lock locally. return this; } } 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 8896f990ecbb..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 @@ -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. @@ -22,12 +22,15 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +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; /** @@ -40,12 +43,13 @@ */ public class BeanFactoryAspectJAdvisorsBuilder { + private static final Log logger = LogFactory.getLog(BeanFactoryAspectJAdvisorsBuilder.class); + private final ListableBeanFactory beanFactory; private final AspectJAdvisorFactory advisorFactory; - @Nullable - private volatile List aspectBeanNames; + private volatile @Nullable List aspectBeanNames; private final Map> advisorsCache = new ConcurrentHashMap<>(); @@ -102,30 +106,37 @@ public List buildAspectJAdvisors() { continue; } if (this.advisorFactory.isAspect(beanType)) { - aspectNames.add(beanName); - AspectMetadata amd = new AspectMetadata(beanType, beanName); - if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { - MetadataAwareAspectInstanceFactory factory = - new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); - List classAdvisors = this.advisorFactory.getAdvisors(factory); - if (this.beanFactory.isSingleton(beanName)) { - this.advisorsCache.put(beanName, classAdvisors); + try { + AspectMetadata amd = new AspectMetadata(beanType, beanName); + if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { + MetadataAwareAspectInstanceFactory factory = + new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); + List classAdvisors = this.advisorFactory.getAdvisors(factory); + if (this.beanFactory.isSingleton(beanName)) { + this.advisorsCache.put(beanName, classAdvisors); + } + else { + this.aspectFactoryCache.put(beanName, factory); + } + advisors.addAll(classAdvisors); } else { + // Per target or per this. + if (this.beanFactory.isSingleton(beanName)) { + throw new IllegalArgumentException("Bean with name '" + beanName + + "' is a singleton, but aspect instantiation model is not singleton"); + } + MetadataAwareAspectInstanceFactory factory = + new PrototypeAspectInstanceFactory(this.beanFactory, beanName); this.aspectFactoryCache.put(beanName, factory); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); } - advisors.addAll(classAdvisors); + aspectNames.add(beanName); } - else { - // Per target or per this. - if (this.beanFactory.isSingleton(beanName)) { - throw new IllegalArgumentException("Bean with name '" + beanName + - "' is a singleton, but aspect instantiation model is not singleton"); + catch (IllegalArgumentException | IllegalStateException | AopConfigException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring incompatible aspect [" + beanType.getName() + "]: " + ex); } - MetadataAwareAspectInstanceFactory factory = - new PrototypeAspectInstanceFactory(this.beanFactory, beanName); - this.aspectFactoryCache.put(beanName, factory); - advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } } @@ -146,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 89a77213e080..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 @@ -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.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; @@ -269,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) { @@ -291,9 +289,9 @@ 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)); + return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass, args)); } private boolean isAspectMaterialized() { 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 4e2f5c3bd801..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-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. @@ -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,7 @@ 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; import org.springframework.util.StringUtils; @@ -95,8 +96,7 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto } - @Nullable - private final BeanFactory beanFactory; + private final @Nullable BeanFactory beanFactory; /** @@ -110,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() @@ -133,17 +133,19 @@ public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstan List advisors = new ArrayList<>(); for (Method method : getAdvisorMethods(aspectClass)) { - // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect - // to getAdvisor(...) to represent the "current position" in the declared methods list. - // However, since Java 7 the "current position" is not valid since the JDK no longer - // returns declared methods in the order in which they are declared in the source code. - // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods - // discovered via reflection in order to support reliable advice ordering across JVM launches. - // Specifically, a value of 0 aligns with the default value used in - // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). - Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); - if (advisor != null) { - advisors.add(advisor); + if (method.equals(ClassUtils.getMostSpecificMethod(method, aspectClass))) { + // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect + // to getAdvisor(...) to represent the "current position" in the declared methods list. + // However, since Java 7 the "current position" is not valid since the JDK no longer + // returns declared methods in the order in which they are declared in the source code. + // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods + // discovered via reflection in order to support reliable advice ordering across JVM launches. + // Specifically, a value of 0 aligns with the default value used in + // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). + Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); + if (advisor != null) { + advisors.add(advisor); + } } } @@ -180,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 @@ -198,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()); @@ -210,12 +210,19 @@ public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInsta return null; } - return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, - this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + try { + return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, + this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + } + catch (IllegalArgumentException | IllegalStateException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring incompatible advice method: " + candidateAdviceMethod, ex); + } + return null; + } } - @Nullable - private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { + private @Nullable AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { AspectJAnnotation aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); if (aspectJAnnotation == null) { @@ -233,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(); @@ -296,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 3c56c8722632..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 @@ -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. @@ -28,8 +28,11 @@ import org.springframework.aop.aspectj.AbstractAspectJAdvice; import org.springframework.aop.aspectj.AspectJPointcutAdvisor; import org.springframework.aop.aspectj.AspectJProxyUtils; +import org.springframework.aop.aspectj.ShadowMatchUtils; import org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.core.Ordered; import org.springframework.util.ClassUtils; @@ -44,7 +47,8 @@ * @since 2.0 */ @SuppressWarnings("serial") -public class AspectJAwareAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator { +public class AspectJAwareAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator + implements SmartInitializingSingleton, DisposableBean { private static final Comparator DEFAULT_PRECEDENCE_COMPARATOR = new AspectJPrecedenceComparator(); @@ -97,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 && @@ -108,6 +111,16 @@ protected boolean shouldSkip(Class beanClass, String beanName) { return super.shouldSkip(beanClass, beanName); } + @Override + public void afterSingletonsInstantiated() { + ShadowMatchUtils.clearCache(); + } + + @Override + public void destroy() { + ShadowMatchUtils.clearCache(); + } + /** * Implements AspectJ's {@link PartialComparable} interface for defining partial orderings. 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/AbstractInterceptorDrivenBeanDefinitionDecorator.java b/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java index 28fc6cdbb69b..a97f79cbb11f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.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. @@ -89,7 +89,7 @@ public final BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder defin proxyDefinition.setDecoratedDefinition(targetHolder); proxyDefinition.getPropertyValues().add("target", targetHolder); // create the interceptor names list - proxyDefinition.getPropertyValues().add("interceptorNames", new ManagedList()); + proxyDefinition.getPropertyValues().add("interceptorNames", new ManagedList<>()); // copy autowire settings from original bean definition. proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); proxyDefinition.setPrimary(targetDefinition.isPrimary()); 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 0a40981134ef..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 @@ -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,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); @@ -201,9 +200,8 @@ private void parseAspect(Element aspectElement, ParserContext parserContext) { List beanReferences = new ArrayList<>(); List declareParents = DomUtils.getChildElementsByTagName(aspectElement, DECLARE_PARENTS); - for (int i = METHOD_INDEX; i < declareParents.size(); i++) { - Element declareParentsElement = declareParents.get(i); - beanDefinitions.add(parseDeclareParents(declareParentsElement, parserContext)); + for (Element declareParent : declareParents) { + beanDefinitions.add(parseDeclareParents(declareParent, parserContext)); } // We have to parse "advice" and all the advice kinds in one loop, to get the @@ -405,24 +403,14 @@ else if (pointcut instanceof String beanName) { */ private Class getAdviceClass(Element adviceElement, ParserContext parserContext) { String elementName = parserContext.getDelegate().getLocalName(adviceElement); - if (BEFORE.equals(elementName)) { - return AspectJMethodBeforeAdvice.class; - } - else if (AFTER.equals(elementName)) { - return AspectJAfterAdvice.class; - } - else if (AFTER_RETURNING_ELEMENT.equals(elementName)) { - return AspectJAfterReturningAdvice.class; - } - else if (AFTER_THROWING_ELEMENT.equals(elementName)) { - return AspectJAfterThrowingAdvice.class; - } - else if (AROUND.equals(elementName)) { - return AspectJAroundAdvice.class; - } - else { - throw new IllegalArgumentException("Unknown advice kind [" + elementName + "]."); - } + return switch (elementName) { + case BEFORE -> AspectJMethodBeforeAdvice.class; + case AFTER -> AspectJAfterAdvice.class; + case AFTER_RETURNING_ELEMENT -> AspectJAfterReturningAdvice.class; + case AFTER_THROWING_ELEMENT -> AspectJAfterThrowingAdvice.class; + case AROUND -> AspectJAroundAdvice.class; + default -> throw new IllegalArgumentException("Unknown advice kind [" + elementName + "]."); + }; } /** @@ -464,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/ScopedProxyBeanDefinitionDecorator.java b/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java index e116ec85947a..d51e472b589c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -42,10 +42,8 @@ class ScopedProxyBeanDefinitionDecorator implements BeanDefinitionDecorator { @Override public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { boolean proxyTargetClass = true; - if (node instanceof Element ele) { - if (ele.hasAttribute(PROXY_TARGET_CLASS)) { - proxyTargetClass = Boolean.parseBoolean(ele.getAttribute(PROXY_TARGET_CLASS)); - } + if (node instanceof Element ele && ele.hasAttribute(PROXY_TARGET_CLASS)) { + proxyTargetClass = Boolean.parseBoolean(ele.getAttribute(PROXY_TARGET_CLASS)); } // Register the original bean definition as it will be referenced by the scoped proxy 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 3a74eca980f9..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; @@ -51,7 +52,7 @@ class SpringConfiguredBeanDefinitionParser implements BeanDefinitionParser { @Override - 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 1b021c7cd8d3..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; @@ -135,7 +135,7 @@ else if (advised.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE && * Check whether the given bean is eligible for advising with this * post-processor's {@link Advisor}. *

Delegates to {@link #isEligible(Class)} for target class checking. - * Can be overridden e.g. to specifically exclude certain beans by name. + * Can be overridden, for example, to specifically exclude certain beans by name. *

Note: Only called for regular bean instances but not for existing * proxy instances which implement {@link Advised} and allow for adding * the local {@link Advisor} to the existing proxy's {@link Advisor} chain. 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 cf40782c784a..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; /** @@ -91,7 +86,7 @@ public void setProxyInterfaces(Class[] proxyInterfaces) { /** * Set additional interceptors (or advisors) to be applied before the - * implicit transaction interceptor, e.g. a PerformanceMonitorInterceptor. + * implicit transaction interceptor, for example, a PerformanceMonitorInterceptor. *

You may specify any AOP Alliance MethodInterceptors or other * Spring AOP Advices, as well as Spring AOP Advisors. * @see org.springframework.aop.interceptor.PerformanceMonitorInterceptor @@ -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/Advised.java b/spring-aop/src/main/java/org/springframework/aop/framework/Advised.java index b956f00fca2b..968b474b26cf 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/Advised.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/Advised.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. @@ -56,9 +56,9 @@ public interface Advised extends TargetClassAware { /** * Determine whether the given interface is proxied. - * @param intf the interface to check + * @param ifc the interface to check */ - boolean isInterfaceProxied(Class intf); + boolean isInterfaceProxied(Class ifc); /** * Change the {@code TargetSource} used by this {@code Advised} object. 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 a93781f2655c..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 @@ -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,12 +22,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; 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; @@ -35,12 +35,12 @@ import org.springframework.aop.IntroductionInfo; import org.springframework.aop.Pointcut; import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.SpringProxy; import org.springframework.aop.TargetSource; import org.springframework.aop.support.DefaultIntroductionAdvisor; 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,38 +79,58 @@ 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. */ - private AdvisorChainFactory advisorChainFactory; - - /** Cache with Method as key and advisor chain List as value. */ - private transient Map> methodCache; + @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<>(); + /** + * List of minimal {@link AdvisorKeyEntry} instances, + * to be assigned to the {@link #advisors} field on reduction. + * @since 6.0.10 + * @see #reduceToAdvisorKey + */ + @SuppressWarnings("serial") private List advisorKey = this.advisors; + /** Cache with Method as key and advisor chain List as value. */ + private transient @Nullable Map> methodCache; + + /** Cache with shared interceptors which are not method-specific. */ + private transient volatile @Nullable List cachedInterceptors; + + /** + * Optional field for {@link AopProxy} implementations to store metadata in. + * Used by {@link JdkDynamicAopProxy}. + * @since 6.1.3 + * @see JdkDynamicAopProxy#JdkDynamicAopProxy(AdvisedSupport) + */ + transient volatile @Nullable Object proxyMetadataCache; + /** * No-arg constructor for use as a JavaBean. */ public AdvisedSupport() { - this.advisorChainFactory = DefaultAdvisorChainFactory.INSTANCE; - this.methodCache = new ConcurrentHashMap<>(32); } /** @@ -118,19 +138,9 @@ public AdvisedSupport() { * @param interfaces the proxied interfaces */ public AdvisedSupport(Class... interfaces) { - this(); setInterfaces(interfaces); } - /** - * Internal constructor for {@link #getConfigurationOnlyCopy()}. - * @since 6.0.10 - */ - private AdvisedSupport(AdvisorChainFactory advisorChainFactory, Map> methodCache) { - this.advisorChainFactory = advisorChainFactory; - this.methodCache = methodCache; - } - /** * Set the given object as target. @@ -170,8 +180,7 @@ public void setTargetClass(@Nullable Class targetClass) { } @Override - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { return this.targetSource.getTargetClass(); } @@ -215,15 +224,15 @@ public void setInterfaces(Class... interfaces) { /** * Add a new proxied interface. - * @param intf the additional interface to proxy + * @param ifc the additional interface to proxy */ - public void addInterface(Class intf) { - Assert.notNull(intf, "Interface must not be null"); - if (!intf.isInterface()) { - throw new IllegalArgumentException("[" + intf.getName() + "] is not an interface"); + public void addInterface(Class ifc) { + Assert.notNull(ifc, "Interface must not be null"); + if (!ifc.isInterface()) { + throw new IllegalArgumentException("[" + ifc.getName() + "] is not an interface"); } - if (!this.interfaces.contains(intf)) { - this.interfaces.add(intf); + if (!this.interfaces.contains(ifc)) { + this.interfaces.add(ifc); adviceChanged(); } } @@ -231,12 +240,12 @@ public void addInterface(Class intf) { /** * Remove a proxied interface. *

Does nothing if the given interface isn't proxied. - * @param intf the interface to remove from the proxy + * @param ifc the interface to remove from the proxy * @return {@code true} if the interface was removed; {@code false} * if the interface was not found and hence could not be removed */ - public boolean removeInterface(Class intf) { - return this.interfaces.remove(intf); + public boolean removeInterface(Class ifc) { + return this.interfaces.remove(ifc); } @Override @@ -245,15 +254,37 @@ public Class[] getProxiedInterfaces() { } @Override - public boolean isInterfaceProxied(Class intf) { + public boolean isInterfaceProxied(Class ifc) { for (Class proxyIntf : this.interfaces) { - if (intf.isAssignableFrom(proxyIntf)) { + if (ifc.isAssignableFrom(proxyIntf)) { + return true; + } + } + return false; + } + + boolean hasUserSuppliedInterfaces() { + for (Class ifc : this.interfaces) { + if (!SpringProxy.class.isAssignableFrom(ifc) && !isAdvisorIntroducedInterface(ifc)) { return true; } } return false; } + private boolean isAdvisorIntroducedInterface(Class ifc) { + for (Advisor advisor : this.advisors) { + if (advisor instanceof IntroductionAdvisor introductionAdvisor) { + for (Class introducedInterface : introductionAdvisor.getInterfaces()) { + if (introducedInterface == ifc) { + return true; + } + } + } + } + return false; + } + @Override public final Advisor[] getAdvisors() { @@ -362,8 +393,7 @@ public void addAdvisors(Collection advisors) { private void validateIntroductionAdvisor(IntroductionAdvisor advisor) { advisor.validateInterfaces(); // If the advisor passed validation, we can make the change. - Class[] ifcs = advisor.getInterfaces(); - for (Class ifc : ifcs) { + for (Class ifc : advisor.getInterfaces()) { addInterface(ifc); } } @@ -482,15 +512,45 @@ public int countAdvicesOfType(@Nullable Class adviceClass) { * @return a List of MethodInterceptors (may also include InterceptorAndDynamicMethodMatchers) */ public List getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class targetClass) { - return this.methodCache.computeIfAbsent(new MethodCacheKey(method), k -> - this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(this, method, targetClass)); + List cachedInterceptors; + if (this.methodCache != null) { + // Method-specific cache for method-specific pointcuts + MethodCacheKey cacheKey = new MethodCacheKey(method); + cachedInterceptors = this.methodCache.get(cacheKey); + if (cachedInterceptors == null) { + cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice( + this, method, targetClass); + this.methodCache.put(cacheKey, cachedInterceptors); + } + } + else { + // Shared cache since there are no method-specific advisors (see below). + cachedInterceptors = this.cachedInterceptors; + if (cachedInterceptors == null) { + cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice( + this, method, targetClass); + this.cachedInterceptors = cachedInterceptors; + } + } + return cachedInterceptors; } /** * Invoked when advice has changed. */ protected void adviceChanged() { - this.methodCache.clear(); + this.methodCache = null; + this.cachedInterceptors = null; + this.proxyMetadataCache = null; + + // Initialize method cache if necessary; otherwise, + // cachedInterceptors is going to be shared (see above). + for (Advisor advisor : this.advisors) { + if (advisor instanceof PointcutAdvisor) { + this.methodCache = new ConcurrentHashMap<>(); + break; + } + } } /** @@ -529,21 +589,28 @@ protected void copyConfigurationFrom(AdvisedSupport other, TargetSource targetSo * replacing the {@link TargetSource}. */ AdvisedSupport getConfigurationOnlyCopy() { - AdvisedSupport copy = new AdvisedSupport(this.advisorChainFactory, this.methodCache); + AdvisedSupport copy = new AdvisedSupport(); copy.copyFrom(this); copy.targetSource = EmptyTargetSource.forClass(getTargetClass(), getTargetSource().isStatic()); + copy.preFiltered = this.preFiltered; + copy.advisorChainFactory = this.advisorChainFactory; copy.interfaces = new ArrayList<>(this.interfaces); copy.advisors = new ArrayList<>(this.advisors); copy.advisorKey = new ArrayList<>(this.advisors.size()); for (Advisor advisor : this.advisors) { copy.advisorKey.add(new AdvisorKeyEntry(advisor)); } + copy.methodCache = this.methodCache; + copy.cachedInterceptors = this.cachedInterceptors; + copy.proxyMetadataCache = this.proxyMetadataCache; return copy; } void reduceToAdvisorKey() { this.advisors = this.advisorKey; - this.methodCache = Collections.emptyMap(); + this.methodCache = null; + this.cachedInterceptors = null; + this.proxyMetadataCache = null; } Object getAdvisorKey() { @@ -551,18 +618,6 @@ Object getAdvisorKey() { } - //--------------------------------------------------------------------- - // Serialization support - //--------------------------------------------------------------------- - - private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { - // Rely on default serialization; just initialize state after deserialization. - ois.defaultReadObject(); - - // Initialize transient fields. - this.methodCache = new ConcurrentHashMap<>(32); - } - @Override public String toProxyConfigString() { return toString(); @@ -584,6 +639,19 @@ public String toString() { } + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize method cache if necessary. + adviceChanged(); + } + + /** * Simple wrapper class around a Method. Used as the key when * caching methods, for efficient equals and hashCode comparisons. @@ -601,7 +669,8 @@ public MethodCacheKey(Method method) { @Override public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof MethodCacheKey that && this.method == that.method)); + return (this == other || (other instanceof MethodCacheKey that && + (this.method == that.method || this.method.equals(that.method)))); } @Override @@ -633,16 +702,13 @@ public int compareTo(MethodCacheKey other) { * @see #getConfigurationOnlyCopy() * @see #getAdvisorKey() */ - private static class AdvisorKeyEntry implements Advisor { + private static final class AdvisorKeyEntry implements Advisor { private final Class adviceType; - @Nullable - private final String classFilterKey; - - @Nullable - private final String methodMatcherKey; + private final @Nullable String classFilterKey; + 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 5230f0fc6396..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-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,13 +30,16 @@ 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; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.CallbackFilter; @@ -46,15 +49,14 @@ import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import org.springframework.cglib.proxy.NoOp; +import org.springframework.cglib.transform.impl.UndeclaredThrowableStrategy; 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; import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; /** * CGLIB-based {@link AopProxy} implementation for the Spring AOP framework. @@ -94,23 +96,27 @@ class CglibAopProxy implements AopProxy, Serializable { private static final int INVOKE_HASHCODE = 6; + private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; + + private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + "kotlinx.coroutines.reactor.MonoKt", CglibAopProxy.class.getClassLoader()); + + private static final GeneratorStrategy undeclaredThrowableStrategy = + new UndeclaredThrowableStrategy(UndeclaredThrowableException.class); + /** Logger available to subclasses; static to optimize serialization. */ protected static final Log logger = LogFactory.getLog(CglibAopProxy.class); /** Keeps track of the Classes that we have validated for final methods. */ private static final Map, Boolean> validatedClasses = new WeakHashMap<>(); - private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; - /** 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; @@ -137,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"); } @@ -198,8 +204,11 @@ 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.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader)); + enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts()); + enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ? + new ClassLoaderAwareGeneratorStrategy(classLoader) : + new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy) + ); Callback[] callbacks = getCallbacks(rootClass); Class[] types = new Class[callbacks.length]; @@ -281,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: " + @@ -405,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 @@ -417,11 +431,11 @@ private static Object processReturnType( returnValue = proxy; } Class returnType = method.getReturnType(); - if (returnValue == null && returnType != Void.TYPE && returnType.isPrimitive()) { + if (returnValue == null && returnType != void.class && returnType.isPrimitive()) { throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } - if (KotlinDetector.isSuspendingFunction(method)) { + if (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(method)) { return COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()) ? CoroutinesUtils.asFlow(returnValue) : CoroutinesUtils.awaitSingleOrNull(returnValue, arguments[arguments.length - 1]); @@ -445,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); } @@ -467,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); @@ -504,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); @@ -532,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 { @@ -558,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; } } @@ -645,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) { @@ -660,10 +664,9 @@ public FixedChainStaticTargetInterceptor( } @Override - @Nullable - public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { - MethodInvocation invocation = new CglibMethodInvocation( - proxy, this.target, method, args, this.targetClass, this.adviceChain, methodProxy); + 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. Object retVal = invocation.proceed(); retVal = processReturnType(proxy, this.target, method, args, retVal); @@ -685,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; @@ -709,12 +711,12 @@ 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 { // We need to create a method invocation... - retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); + retVal = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain).proceed(); } return processReturnType(proxy, target, method, args, retVal); } @@ -746,46 +748,6 @@ public int hashCode() { } - /** - * Implementation of AOP Alliance MethodInvocation used by this AOP proxy. - */ - private static class CglibMethodInvocation extends ReflectiveMethodInvocation { - - public CglibMethodInvocation(Object proxy, @Nullable Object target, Method method, - Object[] arguments, @Nullable Class targetClass, - List interceptorsAndDynamicMethodMatchers, MethodProxy methodProxy) { - - super(proxy, target, method, arguments, targetClass, interceptorsAndDynamicMethodMatchers); - } - - @Override - @Nullable - public Object proceed() throws Throwable { - try { - return super.proceed(); - } - catch (RuntimeException ex) { - throw ex; - } - catch (Exception ex) { - if (ReflectionUtils.declaresException(getMethod(), ex.getClass()) || - KotlinDetector.isKotlinType(getMethod().getDeclaringClass())) { - // Propagate original exception if declared on the target method - // (with callers expecting it). Always propagate it for Kotlin code - // since checked exceptions do not have to be explicitly declared there. - throw ex; - } - else { - // Checked exception thrown in the interceptor but not declared on the - // target method signature -> apply an UndeclaredThrowableException, - // aligned with standard JDK dynamic proxy behavior. - throw new UndeclaredThrowableException(ex); - } - } - } - } - - /** * CallbackFilter to assign Callbacks to methods. */ 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 092099d2e63b..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 @@ -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,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. @@ -32,14 +32,18 @@ */ abstract class CoroutinesUtils { - static Object asFlow(Object publisher) { - return ReactiveFlowKt.asFlow((Publisher) publisher); + static Object asFlow(@Nullable Object publisher) { + if (publisher instanceof Publisher rsPublisher) { + return ReactiveFlowKt.asFlow(rsPublisher); + } + else { + throw new IllegalArgumentException("Not a Reactive Streams Publisher: " + publisher); + } } - @Nullable @SuppressWarnings({"unchecked", "rawtypes"}) - static Object awaitSingleOrNull(Object value, Object continuation) { - return MonoKt.awaitSingleOrNull(value instanceof Mono mono ? mono : Mono.just(value), + 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/DefaultAopProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java index f97455dfc45c..232e3bf13c46 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.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,7 +19,6 @@ import java.io.Serializable; import java.lang.reflect.Proxy; -import org.springframework.aop.SpringProxy; import org.springframework.util.ClassUtils; /** @@ -59,13 +58,14 @@ public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { @Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { - if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { + if (config.isOptimize() || config.isProxyTargetClass() || !config.hasUserSuppliedInterfaces()) { Class targetClass = config.getTargetClass(); - if (targetClass == null) { + if (targetClass == null && config.getProxiedInterfaces().length == 0) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } - if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { + if (targetClass == null || targetClass.isInterface() || + Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); @@ -75,14 +75,4 @@ public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException } } - /** - * Determine whether the supplied {@link AdvisedSupport} has only the - * {@link org.springframework.aop.SpringProxy} interface specified - * (or no proxy interfaces specified at all). - */ - private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) { - Class[] ifcs = config.getProxiedInterfaces(); - return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0]))); - } - } 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 4ab6a89b231a..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-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.aop.framework; +import java.io.IOException; +import java.io.ObjectInputStream; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -25,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; @@ -33,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; @@ -71,34 +73,19 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa private static final long serialVersionUID = 5531744639992436476L; - /* - * NOTE: We could avoid the code duplication between this class and the CGLIB - * proxies by refactoring "invoke" into a template method. However, this approach - * adds at least 10% performance overhead versus a copy-paste solution, so we sacrifice - * elegance for performance (we have a good test suite to ensure that the different - * proxies behave the same :-)). - * This way, we can also more easily take advantage of minor optimizations in each class. - */ + private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; + + private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + "kotlinx.coroutines.reactor.MonoKt", JdkDynamicAopProxy.class.getClassLoader()); /** We use a static Log to avoid serialization issues. */ private static final Log logger = LogFactory.getLog(JdkDynamicAopProxy.class); - private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; - /** Config used to configure this proxy. */ private final AdvisedSupport advised; - private final Class[] proxiedInterfaces; - - /** - * Is the {@link #equals} method defined on the proxied interfaces? - */ - private boolean equalsDefined; - - /** - * Is the {@link #hashCode} method defined on the proxied interfaces? - */ - private boolean hashCodeDefined; + /** Cached in {@link AdvisedSupport#proxyMetadataCache}. */ + private transient ProxiedInterfacesCache cache; /** @@ -110,8 +97,17 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { Assert.notNull(config, "AdvisedSupport must not be null"); this.advised = config; - this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); - findDefinedEqualsAndHashCodeMethods(this.proxiedInterfaces); + + // Initialize ProxiedInterfacesCache if not cached already + ProxiedInterfacesCache cache; + if (config.proxyMetadataCache instanceof ProxiedInterfacesCache proxiedInterfacesCache) { + cache = proxiedInterfacesCache; + } + else { + cache = new ProxiedInterfacesCache(config); + config.proxyMetadataCache = cache; + } + this.cache = cache; } @@ -125,13 +121,13 @@ public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isTraceEnabled()) { logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); } - return Proxy.newProxyInstance(determineClassLoader(classLoader), this.proxiedInterfaces, this); + return Proxy.newProxyInstance(determineClassLoader(classLoader), this.cache.proxiedInterfaces, this); } @SuppressWarnings("deprecation") @Override public Class getProxyClass(@Nullable ClassLoader classLoader) { - return Proxy.getProxyClass(determineClassLoader(classLoader), this.proxiedInterfaces); + return Proxy.getProxyClass(determineClassLoader(classLoader), this.cache.proxiedInterfaces); } /** @@ -160,28 +156,6 @@ private ClassLoader determineClassLoader(@Nullable ClassLoader classLoader) { return classLoader; } - /** - * Finds any {@link #equals} or {@link #hashCode} method that may be defined - * on the supplied set of interfaces. - * @param proxiedInterfaces the interfaces to introspect - */ - private void findDefinedEqualsAndHashCodeMethods(Class[] proxiedInterfaces) { - for (Class proxiedInterface : proxiedInterfaces) { - Method[] methods = proxiedInterface.getDeclaredMethods(); - for (Method method : methods) { - if (AopUtils.isEqualsMethod(method)) { - this.equalsDefined = true; - } - if (AopUtils.isHashCodeMethod(method)) { - this.hashCodeDefined = true; - } - if (this.equalsDefined && this.hashCodeDefined) { - return; - } - } - } - } - /** * Implementation of {@code InvocationHandler.invoke}. @@ -189,8 +163,7 @@ private void findDefinedEqualsAndHashCodeMethods(Class[] proxiedInterfaces) { * 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; @@ -198,11 +171,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl Object target = null; try { - if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + if (!this.cache.equalsDefined && AopUtils.isEqualsMethod(method)) { // The target does not implement the equals(Object) method itself. return equals(args[0]); } - else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + else if (!this.cache.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { // The target does not implement the hashCode() method itself. return hashCode(); } @@ -238,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 { @@ -259,11 +232,11 @@ else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && // a reference to itself in another returned object. retVal = proxy; } - else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + else if (retVal == null && returnType != void.class && returnType.isPrimitive()) { throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } - if (KotlinDetector.isSuspendingFunction(method)) { + if (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(method)) { return COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()) ? CoroutinesUtils.asFlow(retVal) : CoroutinesUtils.awaitSingleOrNull(retVal, args[args.length - 1]); } @@ -324,4 +297,63 @@ public int hashCode() { return JdkDynamicAopProxy.class.hashCode() * 13 + this.advised.getTargetSource().hashCode(); } + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + this.cache = new ProxiedInterfacesCache(this.advised); + } + + + /** + * Holder for the complete proxied interfaces and derived metadata, + * to be cached in {@link AdvisedSupport#proxyMetadataCache}. + * @since 6.1.3 + */ + private static final class ProxiedInterfacesCache { + + final Class[] proxiedInterfaces; + + final boolean equalsDefined; + + final boolean hashCodeDefined; + + ProxiedInterfacesCache(AdvisedSupport config) { + this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(config, true); + + // Find any {@link #equals} or {@link #hashCode} method that may be defined + // on the supplied set of interfaces. + boolean equalsDefined = false; + boolean hashCodeDefined = false; + for (Class proxiedInterface : this.proxiedInterfaces) { + Method[] methods = proxiedInterface.getDeclaredMethods(); + for (Method method : methods) { + if (AopUtils.isEqualsMethod(method)) { + equalsDefined = true; + if (hashCodeDefined) { + break; + } + } + if (AopUtils.isHashCodeMethod(method)) { + hashCodeDefined = true; + if (equalsDefined) { + break; + } + } + } + if (equalsDefined && hashCodeDefined) { + break; + } + } + this.equalsDefined = equalsDefined; + this.hashCodeDefined = hashCodeDefined; + } + } + } 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 cc29883d590a..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 @@ -47,7 +47,7 @@ * *

NOTE: This class is considered internal and should not be * directly accessed. The sole reason for it being public is compatibility - * with existing framework integrations (e.g. Pitchfork). For any other + * with existing framework integrations (for example, Pitchfork). For any other * purposes, use the {@link ProxyMethodInvocation} interface instead. * * @author Rod Johnson @@ -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 ca048cb9f17c..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 @@ -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,13 +18,16 @@ 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; import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanCreationException; 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; /** @@ -51,8 +54,7 @@ @SuppressWarnings("serial") public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator { - @Nullable - private BeanFactoryAdvisorRetrievalHelper advisorRetrievalHelper; + private @Nullable BeanFactoryAdvisorRetrievalHelper advisorRetrievalHelper; @Override @@ -71,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); @@ -97,7 +98,13 @@ protected List findEligibleAdvisors(Class beanClass, String beanName List eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); extendAdvisors(eligibleAdvisors); if (!eligibleAdvisors.isEmpty()) { - eligibleAdvisors = sortAdvisors(eligibleAdvisors); + try { + eligibleAdvisors = sortAdvisors(eligibleAdvisors); + } + catch (BeanCreationException ex) { + throw new AopConfigException("Advisor sorting failed with unexpected bean creation, probably due " + + "to custom use of the Ordered interface. Consider using the @Order annotation instead.", ex); + } } return eligibleAdvisors; } 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 611a1bac8b9b..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 @@ -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,7 +20,6 @@ import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -29,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; @@ -49,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; @@ -72,7 +71,7 @@ * Instead of x repetitive proxy definitions for x target beans, you can register * one single such post processor with the bean factory to achieve the same effect. * - *

Subclasses can apply any strategy to decide if a bean is to be proxied, e.g. by type, + *

Subclasses can apply any strategy to decide if a bean is to be proxied, for example, by type, * by name, by definition details, etc. They can also return additional interceptors that * should just be applied to the specific bean instance. A simple concrete implementation is * {@link BeanNameAutoProxyCreator}, identifying the beans to be proxied via given names. @@ -102,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 @@ -130,13 +128,11 @@ 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 = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + private final Set targetSourcedBeans = ConcurrentHashMap.newKeySet(16); private final Map earlyBeanReferences = new ConcurrentHashMap<>(16); @@ -216,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; } @@ -257,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; } @@ -270,7 +263,7 @@ public Object getEarlyBeanReference(Object bean, String beanName) { } @Override - 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,7 +304,7 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str * @see #getAdvicesAndAdvisorsForBean */ @Override - 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) { @@ -402,7 +395,7 @@ protected boolean isInfrastructureClass(Class beanClass) { /** * Subclasses should override this method to return {@code true} if the * given bean should not be considered for auto-proxying by this post-processor. - *

Sometimes we need to be able to avoid this happening, e.g. if it will lead to + *

Sometimes we need to be able to avoid this happening, for example, if it will lead to * a circular reference or if the existing target instance needs to be preserved. * This implementation returns {@code false} unless the bean name indicates an * "original instance" according to {@code AutowireCapableBeanFactory} conventions. @@ -425,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)) { @@ -459,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); @@ -553,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(); @@ -618,7 +610,7 @@ protected void customizeProxyFactory(ProxyFactory proxyFactory) { /** * Return whether the given bean is to be proxied, what additional - * advices (e.g. AOP Alliance interceptors) and advisors to apply. + * advices (for example, AOP Alliance interceptors) and advisors to apply. * @param beanClass the class of the bean to advise * @param beanName the name of the bean * @param customTargetSource the TargetSource returned by the @@ -632,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 1de9382a2e2c..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; /** @@ -47,7 +48,7 @@ public abstract class AutoProxyUtils { /** * Bean definition attribute that indicates the original target class of an - * auto-proxied bean, e.g. to be used for the introspection of annotations + * auto-proxied bean, for example, to be used for the introspection of annotations * on the target class behind an interface-based proxy. * @since 4.2.3 * @see #determineTargetClass @@ -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 823961064347..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,19 +49,18 @@ public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator { private static final String[] NO_ALIASES = new String[0]; - @Nullable - private List beanNames; + private @Nullable List beanNames; /** * Set the names of the beans that should automatically get wrapped with proxies. - * A name can specify a prefix to match by ending with "*", e.g. "myBean,tx*" + * A name can specify a prefix to match by ending with "*", for example, "myBean,tx*" * will match the bean named "myBean" and all beans whose name start with "tx". *

NOTE: In case of a FactoryBean, only the objects created by the * FactoryBean will get proxied. This default behavior applies as of Spring 2.0. * If you intend to proxy a FactoryBean instance itself (a rare use case, but * Spring 1.2's default behavior), specify the bean name of the FactoryBean - * including the factory-bean prefix "&": e.g. "&myFactoryBean". + * including the factory-bean prefix "&": for example, "&myFactoryBean". * @see org.springframework.beans.factory.FactoryBean * @see org.springframework.beans.factory.BeanFactory#FACTORY_BEAN_PREFIX */ @@ -81,7 +81,7 @@ public void setBeanNames(String... beanNames) { * @see #setBeanNames(String...) */ @Override - protected TargetSource getCustomTargetSource(Class beanClass, String beanName) { + protected @Nullable TargetSource getCustomTargetSource(Class beanClass, String beanName) { return (isSupportedBeanName(beanClass, beanName) ? super.getCustomTargetSource(beanClass, beanName) : null); } @@ -92,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) ? @@ -113,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 650b40736cd6..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 @@ -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,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) { @@ -151,8 +148,7 @@ protected DefaultListableBeanFactory buildInternalBeanFactory(ConfigurableBeanFa // Filter out BeanPostProcessors that are part of the AOP infrastructure, // since those are only meant to apply to beans defined in the original factory. - internalBeanFactory.getBeanPostProcessors().removeIf(beanPostProcessor -> - beanPostProcessor instanceof AopInfrastructureBean); + internalBeanFactory.getBeanPostProcessors().removeIf(AopInfrastructureBean.class::isInstance); return internalBeanFactory; } @@ -196,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 5d35e87994b5..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-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. @@ -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; @@ -52,12 +52,13 @@ *

Provides support for executor qualification on a method-by-method basis. * {@code AsyncExecutionAspectSupport} objects must be constructed with a default {@code * Executor}, but each individual method may further qualify a specific {@code Executor} - * bean to be used when executing it, e.g. through an annotation attribute. + * bean to be used when executing it, for example, through an annotation attribute. * * @author Chris Beams * @author Juergen Hoeller * @author Stephane Nicoll * @author He Bo + * @author Sebastien Deleuze * @since 3.1.2 */ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { @@ -73,17 +74,16 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { protected final Log logger = LogFactory.getLog(getClass()); - private final Map executors = new ConcurrentHashMap<>(16); - private SingletonSupplier defaultExecutor; private SingletonSupplier exceptionHandler; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; + + private @Nullable StringValueResolver embeddedValueResolver; + + private final Map executors = new ConcurrentHashMap<>(16); - @Nullable - private StringValueResolver embeddedValueResolver; /** * Create a new instance with a default {@link AsyncUncaughtExceptionHandler}. @@ -116,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); @@ -157,6 +157,7 @@ public void setBeanFactory(BeanFactory beanFactory) { if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) { this.embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory); } + this.executors.clear(); } @@ -164,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; @@ -200,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. @@ -210,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 + "'"); @@ -231,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 @@ -278,19 +275,14 @@ 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) */ - @Nullable - @SuppressWarnings("deprecation") - 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); } - else if (void.class == returnType) { + else if (void.class == returnType || "kotlin.Unit".equals(returnType.getName())) { executor.submit(task); return null; } @@ -312,7 +304,7 @@ else if (void.class == returnType) { * @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 e89040d1f1e0..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 @@ -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 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,8 +32,6 @@ import org.springframework.core.Ordered; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * AOP Alliance {@code MethodInterceptor} that processes method invocations @@ -98,13 +97,11 @@ public AsyncExecutionInterceptor(@Nullable Executor defaultExecutor, AsyncUncaug * otherwise. */ @Override - @Nullable - 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); - Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); - final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + final Method userMethod = BridgeMethodResolver.getMostSpecificMethod(invocation.getMethod(), targetClass); - AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod); + AsyncTaskExecutor executor = determineAsyncExecutor(userMethod); if (executor == null) { throw new IllegalStateException( "No executor specified and no default executor set on AsyncExecutionInterceptor either"); @@ -118,15 +115,16 @@ public Object invoke(final MethodInvocation invocation) throws Throwable { } } catch (ExecutionException ex) { - handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); + Throwable cause = ex.getCause(); + handleError(cause == null ? ex : cause, userMethod, invocation.getArguments()); } catch (Throwable ex) { - handleError(ex, userDeclaredMethod, invocation.getArguments()); + handleError(ex, userMethod, invocation.getArguments()); } return null; }; - return doSubmit(task, executor, invocation.getMethod().getReturnType()); + return doSubmit(task, executor, userMethod.getReturnType()); } /** @@ -141,22 +139,20 @@ 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; } /** * This implementation searches for a unique {@link org.springframework.core.task.TaskExecutor} * bean in the context, or for an {@link Executor} bean named "taskExecutor" otherwise. - * If neither of the two is resolvable (e.g. if no {@code BeanFactory} was configured at all), + * If neither of the two is resolvable (for example, if no {@code BeanFactory} was configured at all), * this implementation falls back to a newly created {@link SimpleAsyncTaskExecutor} instance * for local use if no default could be found. * @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 dd802ce813bd..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; /** @@ -31,7 +31,7 @@ *

Can be applied to methods of local services that involve heavy use * of system resources, in a scenario where it is more efficient to * throttle concurrency for a specific service rather than restricting - * the entire thread pool (e.g. the web container's thread pool). + * the entire thread pool (for example, the web container's thread pool). * *

The default concurrency limit of this interceptor is 1. * Specify the "concurrencyLimit" bean property to change this value. @@ -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 9822374da1a3..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,17 +20,17 @@ 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} * as a thread-local object. We occasionally need to do this; for example, when a pointcut - * (e.g. an AspectJ expression pointcut) needs to know the full invocation context. + * (for example, an AspectJ expression pointcut) needs to know the full invocation context. * *

Don't use this interceptor unless this is really necessary. Target objects should * not normally know about Spring AOP, as this creates a dependency on Spring API. @@ -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 63f40110c4bb..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 @@ -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,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,7 +53,8 @@ class ScopedProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProc @Override - 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()); @@ -71,15 +72,13 @@ 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(ConfigurableBeanFactory beanFactory, - @Nullable String targetBeanName) { + private @Nullable BeanDefinition getTargetBeanDefinition( + ConfigurableBeanFactory beanFactory, @Nullable String targetBeanName) { if (targetBeanName != null && beanFactory.containsBean(targetBeanName)) { return beanFactory.getMergedBeanDefinition(targetBeanName); @@ -122,40 +121,32 @@ public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationConte @Override public CodeBlock generateSetBeanDefinitionPropertiesCode( - GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, Predicate attributeFilter) { - RootBeanDefinition processedBeanDefinition = new RootBeanDefinition( - beanDefinition); - processedBeanDefinition - .setTargetType(this.targetBeanDefinition.getResolvableType()); - processedBeanDefinition.getPropertyValues() - .removePropertyValue("targetBeanName"); + RootBeanDefinition processedBeanDefinition = new RootBeanDefinition(beanDefinition); + processedBeanDefinition.setTargetType(this.targetBeanDefinition.getResolvableType()); + processedBeanDefinition.getPropertyValues().removePropertyValue("targetBeanName"); return super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, processedBeanDefinition, attributeFilter); } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + boolean allowDirectSupplierShortcut) { GeneratedMethod generatedMethod = beanRegistrationCode.getMethods() .add("getScopedProxyInstance", method -> { - method.addJavadoc( - "Create the scoped proxy bean instance for '$L'.", + method.addJavadoc("Create the scoped proxy bean instance for '$L'.", this.registeredBean.getBeanName()); method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); method.returns(ScopedProxyFactoryBean.class); - method.addParameter(RegisteredBean.class, - REGISTERED_BEAN_PARAMETER_NAME); + method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER_NAME); method.addStatement("$T factory = new $T()", - ScopedProxyFactoryBean.class, - ScopedProxyFactoryBean.class); - method.addStatement("factory.setTargetBeanName($S)", - this.targetBeanName); - method.addStatement( - "factory.setBeanFactory($L.getBeanFactory())", + ScopedProxyFactoryBean.class, ScopedProxyFactoryBean.class); + method.addStatement("factory.setTargetBeanName($S)", this.targetBeanName); + method.addStatement("factory.setBeanFactory($L.getBeanFactory())", REGISTERED_BEAN_PARAMETER_NAME); method.addStatement("return factory"); }); 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 fe7934b0ca0c..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,7 +116,7 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override - public Object getObject() { + public @Nullable Object getObject() { if (this.proxy == null) { throw new FactoryBeanNotInitializedException(); } @@ -125,7 +124,7 @@ public Object getObject() { } @Override - 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 968cea81833d..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,13 +16,15 @@ 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; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -128,6 +130,7 @@ public static String getOriginalBeanName(@Nullable String targetBeanName) { * the target bean within a scoped proxy. * @since 4.1.4 */ + @Contract("null -> false") public static boolean isScopedTarget(@Nullable String beanName) { return (beanName != null && beanName.startsWith(TARGET_NAME_PREFIX)); } 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 f9efdc469ada..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 @@ -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,11 +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.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -43,16 +42,13 @@ @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 volatile Object adviceMonitor = new Object(); + private transient Object adviceMonitor = new Object(); /** @@ -70,24 +66,13 @@ 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; } @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; - resetAdviceMonitor(); - } - - private void resetAdviceMonitor() { - if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { - this.adviceMonitor = cbf.getSingletonMutex(); - } - else { - this.adviceMonitor = new Object(); - } } /** @@ -118,9 +103,7 @@ public Advice getAdvice() { return advice; } else { - // No singleton guarantees from the factory -> let's lock locally but - // reuse the factory's singleton lock, just in case a lazy dependency - // of our advice bean happens to trigger the singleton lock implicitly... + // No singleton guarantees from the factory -> let's lock locally. synchronized (this.adviceMonitor) { advice = this.advice; if (advice == null) { @@ -155,7 +138,7 @@ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFound ois.defaultReadObject(); // Initialize transient fields. - resetAdviceMonitor(); + this.adviceMonitor = new Object(); } } 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 30fead6732fe..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; @@ -29,7 +30,7 @@ * Abstract base regular expression pointcut bean. JavaBean properties are: *

    *
  • pattern: regular expression for the fully-qualified method names to match. - * The exact regexp syntax will depend on the subclass (e.g. Perl5 regular expressions) + * The exact regexp syntax will depend on the subclass (for example, Perl5 regular expressions) *
  • patterns: alternative property taking a String array of patterns. * The result will be the union of these patterns. *
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 e30e4519c713..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import kotlin.coroutines.Continuation; import kotlin.coroutines.CoroutineContext; import kotlinx.coroutines.Job; -import org.reactivestreams.Publisher; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.AopInvocationException; @@ -43,7 +43,7 @@ import org.springframework.core.CoroutinesUtils; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodIntrospector; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -65,6 +65,10 @@ */ public abstract class AopUtils { + private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + "kotlinx.coroutines.reactor.MonoKt", AopUtils.class.getClassLoader()); + + /** * Check whether the given object is a JDK dynamic proxy or a CGLIB proxy. *

This method additionally checks if the given object is an instance @@ -73,6 +77,7 @@ public abstract class AopUtils { * @see #isJdkDynamicProxy * @see #isCglibProxy */ + @Contract("null -> false") public static boolean isAopProxy(@Nullable Object object) { return (object instanceof SpringProxy && (Proxy.isProxyClass(object.getClass()) || object.getClass().getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR))); @@ -86,6 +91,7 @@ public static boolean isAopProxy(@Nullable Object object) { * @param object the object to check * @see java.lang.reflect.Proxy#isProxyClass */ + @Contract("null -> false") public static boolean isJdkDynamicProxy(@Nullable Object object) { return (object instanceof SpringProxy && Proxy.isProxyClass(object.getClass())); } @@ -98,6 +104,7 @@ public static boolean isJdkDynamicProxy(@Nullable Object object) { * @param object the object to check * @see ClassUtils#isCglibProxy(Object) */ + @Contract("null -> false") public static boolean isCglibProxy(@Nullable Object object) { return (object instanceof SpringProxy && object.getClass().getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)); @@ -187,24 +194,23 @@ public static boolean isFinalizeMethod(@Nullable Method method) { /** * Given a method, which may come from an interface, and a target class used * in the current AOP invocation, find the corresponding target method if there - * is one. E.g. the method may be {@code IFoo.bar()} and the target class + * is one. For example, the method may be {@code IFoo.bar()} and the target class * may be {@code DefaultFoo}. In this case, the method may be * {@code DefaultFoo.bar()}. This enables attributes on that method to be found. *

NOTE: In contrast to {@link org.springframework.util.ClassUtils#getMostSpecificMethod}, * this method resolves bridge methods in order to retrieve attributes from * the original method definition. * @param method the method to be invoked, which may come from an interface - * @param targetClass the target class for the current invocation. - * May be {@code null} or may not even implement the method. + * @param targetClass the target class for the current invocation + * (can be {@code null} or may not even implement the method) * @return the specific target method, or the original method if the - * {@code targetClass} doesn't implement it or is {@code null} + * {@code targetClass} does not implement it * @see org.springframework.util.ClassUtils#getMostSpecificMethod + * @see org.springframework.core.BridgeMethodResolver#getMostSpecificMethod */ public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { Class specificTargetClass = (targetClass != null ? ClassUtils.getUserClass(targetClass) : null); - Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, specificTargetClass); - // If we are dealing with method with generic parameters, find the original method. - return BridgeMethodResolver.findBridgedMethod(resolvedMethod); + return BridgeMethodResolver.getMostSpecificMethod(method, specificTargetClass); } /** @@ -341,15 +347,15 @@ 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. try { - ReflectionUtils.makeAccessible(method); - return KotlinDetector.isSuspendingFunction(method) ? - KotlinDelegate.invokeSuspendingFunction(method, target, args) : method.invoke(target, args); + Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); + ReflectionUtils.makeAccessible(originalMethod); + return (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(originalMethod) ? + KotlinDelegate.invokeSuspendingFunction(originalMethod, target, args) : originalMethod.invoke(target, args)); } catch (InvocationTargetException ex) { // Invoked method threw a checked exception. @@ -365,18 +371,18 @@ public static Object invokeJoinpointUsingReflection(@Nullable Object target, Met } } + /** * Inner class to avoid a hard dependency on Kotlin at runtime. */ private static class KotlinDelegate { - public static Publisher invokeSuspendingFunction(Method method, 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); return CoroutinesUtils.invokeSuspendingFunction(context, method, target, args); } - } } 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 e04714ed7924..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 @@ -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,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; @@ -52,7 +53,7 @@ public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher /** * The class against which to match. - *

Available for use in subclasses since 6.1. + * @since 6.1 */ protected final Class clazz; @@ -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/Pointcuts.java b/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.java index 50e997266bd9..35f3f644f54c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.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. @@ -101,7 +101,7 @@ private static class SetterPointcut extends StaticMethodMatcherPointcut implemen public boolean matches(Method method, Class targetClass) { return (method.getName().startsWith("set") && method.getParameterCount() == 1 && - method.getReturnType() == Void.TYPE); + method.getReturnType() == void.class); } private Object readResolve() { @@ -127,7 +127,7 @@ private static class GetterPointcut extends StaticMethodMatcherPointcut implemen public boolean matches(Method method, Class targetClass) { return (method.getName().startsWith("get") && method.getParameterCount() == 0 && - method.getReturnType() != Void.TYPE); + method.getReturnType() != void.class); } private Object readResolve() { 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 421063c67fd0..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,6 +88,7 @@ public AnnotationMatchingPointcut(@Nullable Class classAnn * @see AnnotationClassFilter#AnnotationClassFilter(Class, boolean) * @see AnnotationMethodMatcher#AnnotationMethodMatcher(Class, boolean) */ + @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 5bb7929b2dc1..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; @@ -153,16 +150,6 @@ public Class getTargetClass() { } } - @Override - public boolean isStatic() { - return false; - } - - @Override - public void releaseTarget(Object target) throws Exception { - // Nothing to do here. - } - /** * Copy configuration from the other AbstractBeanFactoryBasedTargetSource object. 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 8246e1e6268f..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,16 +66,10 @@ 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); } - @Override - public boolean isStatic() { - return false; - } - /** * Returns the lazy-initialized target object, * creating it on-the-fly if it doesn't exist already. @@ -91,11 +84,6 @@ public synchronized Object getTarget() throws Exception { return this.lazyTarget; } - @Override - public void releaseTarget(Object target) throws Exception { - // nothing to do - } - /** * Subclasses should implement this method to return the lazy initialized object. 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 6b0fdfd49848..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,18 +110,10 @@ public boolean isStatic() { * Always returns {@code null}. */ @Override - @Nullable - public Object getTarget() { + public @Nullable Object getTarget() { return null; } - /** - * Nothing to release. - */ - @Override - public void releaseTarget(Object target) { - } - /** * Returns the canonical instance on deserialization in case 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 1a1a2fa7552a..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; /** @@ -66,21 +67,11 @@ public synchronized Class getTargetClass() { return this.target.getClass(); } - @Override - public final boolean isStatic() { - return false; - } - @Override public synchronized Object getTarget() { return this.target; } - @Override - public void releaseTarget(Object target) { - // nothing to do - } - /** * Swap the target, returning the old target object. 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 e1b2ae6a56c7..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; @@ -67,11 +68,6 @@ public Object getTarget() { return this.target; } - @Override - public void releaseTarget(Object target) { - // nothing to do - } - @Override public boolean isStatic() { return true; 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 32c6ce1c928f..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -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; @@ -73,30 +73,14 @@ public synchronized Class getTargetClass() { return this.targetObject.getClass(); } - /** - * Not static. - */ - @Override - public boolean isStatic() { - return false; - } - @Override - @Nullable - public final synchronized Object getTarget() { + public final synchronized @Nullable Object getTarget() { if ((refreshCheckDelayElapsed() && requiresRefresh()) || this.targetObject == null) { refresh(); } return this.targetObject; } - /** - * No need to release target. - */ - @Override - public void releaseTarget(Object object) { - } - @Override public final synchronized void 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/main/resources/META-INF/spring/aot.factories b/spring-aop/src/main/resources/META-INF/spring/aot.factories index e3e7529ad300..c44e9d5d589f 100644 --- a/spring-aop/src/main/resources/META-INF/spring/aot.factories +++ b/spring-aop/src/main/resources/META-INF/spring/aot.factories @@ -1,5 +1,6 @@ org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ -org.springframework.aop.scope.ScopedProxyBeanRegistrationAotProcessor +org.springframework.aop.scope.ScopedProxyBeanRegistrationAotProcessor,\ +org.springframework.aop.aspectj.annotation.AspectJAdvisorBeanRegistrationAotProcessor org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor= \ org.springframework.aop.aspectj.annotation.AspectJBeanFactoryInitializationAotProcessor diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java new file mode 100644 index 000000000000..f6768b0e8440 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.aspectj; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.function.Consumer; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AbstractAspectJAdvice}. + * + * @author Joshua Chen + * @author Stephane Nicoll + */ +class AbstractAspectJAdviceTests { + + @Test + void setArgumentNamesFromStringArray_withoutJoinPointParameter() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithNoJoinPoint"); + assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2")); + } + + @Test + void setArgumentNamesFromStringArray_withJoinPointAsFirstParameter() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsFirstParameter"); + assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2")); + } + + @Test + void setArgumentNamesFromStringArray_withJoinPointAsLastParameter() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsLastParameter"); + assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2", "THIS_JOIN_POINT")); + } + + @Test + void setArgumentNamesFromStringArray_withJoinPointAsMiddleParameter() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsMiddleParameter"); + assertThat(advice).satisfies(hasArgumentNames("arg1", "THIS_JOIN_POINT", "arg2")); + } + + @Test + void setArgumentNamesFromStringArray_withProceedingJoinPoint() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithProceedingJoinPoint"); + assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2")); + } + + @Test + void setArgumentNamesFromStringArray_withStaticPart() { + AbstractAspectJAdvice advice = getAspectJAdvice("methodWithStaticPart"); + assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2")); + } + + private Consumer hasArgumentNames(String... argumentNames) { + return advice -> assertThat(advice).extracting("argumentNames") + .asInstanceOf(InstanceOfAssertFactories.array(String[].class)) + .containsExactly(argumentNames); + } + + private AbstractAspectJAdvice getAspectJAdvice(final String methodName) { + AbstractAspectJAdvice advice = new TestAspectJAdvice(getMethod(methodName), + mock(AspectJExpressionPointcut.class), mock(AspectInstanceFactory.class)); + advice.setArgumentNamesFromStringArray("arg1", "arg2"); + return advice; + } + + private Method getMethod(final String methodName) { + return Arrays.stream(Sample.class.getDeclaredMethods()) + .filter(method -> method.getName().equals(methodName)).findFirst() + .orElseThrow(); + } + + @SuppressWarnings("serial") + public static class TestAspectJAdvice extends AbstractAspectJAdvice { + + public TestAspectJAdvice(Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut, + AspectInstanceFactory aspectInstanceFactory) { + super(aspectJAdviceMethod, pointcut, aspectInstanceFactory); + } + + @Override + public boolean isBeforeAdvice() { + return false; + } + + @Override + public boolean isAfterAdvice() { + return false; + } + } + + @SuppressWarnings("unused") + static class Sample { + + void methodWithNoJoinPoint(String arg1, String arg2) { + } + + void methodWithJoinPointAsFirstParameter(JoinPoint joinPoint, String arg1, String arg2) { + } + + void methodWithJoinPointAsLastParameter(String arg1, String arg2, JoinPoint joinPoint) { + } + + void methodWithJoinPointAsMiddleParameter(String arg1, JoinPoint joinPoint, String arg2) { + } + + void methodWithProceedingJoinPoint(ProceedingJoinPoint joinPoint, String arg1, String arg2) { + } + + void methodWithStaticPart(JoinPoint.StaticPart staticPart, String arg1, String arg2) { + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java index 16f7b6b7d83e..d361600e6ea5 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.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. @@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Unit tests for {@link AspectJAdviceParameterNameDiscoverer}. + * Tests for {@link AspectJAdviceParameterNameDiscoverer}. * * @author Adrian Colyer * @author Chris Beams diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index e1b2a93b9589..0e93dafdf462 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.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. @@ -23,9 +23,6 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.aspectj.weaver.tools.PointcutExpression; -import org.aspectj.weaver.tools.PointcutPrimitive; -import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import test.annotation.EmptySpringAnnotation; @@ -42,18 +39,16 @@ import org.springframework.beans.testfixture.beans.subpkg.DeepBean; 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; /** * @author Rob Harrop * @author Rod Johnson * @author Chris Beams + * @author Juergen Hoeller + * @author Yanming Zhou */ -public class AspectJExpressionPointcutTests { - - public static final String MATCH_ALL_METHODS = "execution(* *(..))"; +class AspectJExpressionPointcutTests { private Method getAge; @@ -65,7 +60,7 @@ public class AspectJExpressionPointcutTests { @BeforeEach - public void setUp() throws NoSuchMethodException { + void setup() throws NoSuchMethodException { getAge = TestBean.class.getMethod("getAge"); setAge = TestBean.class.getMethod("setAge", int.class); setSomeNumber = TestBean.class.getMethod("setSomeNumber", Number.class); @@ -78,7 +73,7 @@ public void setUp() throws NoSuchMethodException { @Test - public void testMatchExplicit() { + void testMatchExplicit() { String expression = "execution(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; Pointcut pointcut = getPointcut(expression); @@ -96,7 +91,7 @@ public void testMatchExplicit() { } @Test - public void testMatchWithTypePattern() throws Exception { + void testMatchWithTypePattern() { String expression = "execution(* *..TestBean.*Age(..))"; Pointcut pointcut = getPointcut(expression); @@ -115,12 +110,12 @@ public void testMatchWithTypePattern() throws Exception { @Test - public void testThis() throws SecurityException, NoSuchMethodException{ + void testThis() throws SecurityException, NoSuchMethodException{ testThisOrTarget("this"); } @Test - public void testTarget() throws SecurityException, NoSuchMethodException { + void testTarget() throws SecurityException, NoSuchMethodException { testThisOrTarget("target"); } @@ -144,12 +139,12 @@ private void testThisOrTarget(String which) throws SecurityException, NoSuchMeth } @Test - public void testWithinRootPackage() throws SecurityException, NoSuchMethodException { + void testWithinRootPackage() throws SecurityException, NoSuchMethodException { testWithinPackage(false); } @Test - public void testWithinRootAndSubpackages() throws SecurityException, NoSuchMethodException { + void testWithinRootAndSubpackages() throws SecurityException, NoSuchMethodException { testWithinPackage(true); } @@ -173,32 +168,32 @@ private void testWithinPackage(boolean matchSubpackages) throws SecurityExceptio } @Test - public void testFriendlyErrorOnNoLocationClassMatching() { + void testFriendlyErrorOnNoLocationClassMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(ITestBean.class)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getClassFilter().matches(ITestBean.class)) + .withMessageContaining("expression"); } @Test - public void testFriendlyErrorOnNoLocation2ArgMatching() { + void testFriendlyErrorOnNoLocation2ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(getAge, ITestBean.class)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class)) + .withMessageContaining("expression"); } @Test - public void testFriendlyErrorOnNoLocation3ArgMatching() { + void testFriendlyErrorOnNoLocation3ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(getAge, ITestBean.class, (Object[]) null)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class, (Object[]) null)) + .withMessageContaining("expression"); } @Test - public void testMatchWithArgs() throws Exception { + void testMatchWithArgs() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number)) && args(Double)"; Pointcut pointcut = getPointcut(expression); @@ -210,14 +205,16 @@ public void testMatchWithArgs() throws Exception { // not currently testable in a reliable fashion //assertDoesNotMatchStringClass(classFilter); - assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 12D)).as("Should match with setSomeNumber with Double input").isTrue(); - assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 11)).as("Should not match setSomeNumber with Integer input").isFalse(); + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 12D)) + .as("Should match with setSomeNumber with Double input").isTrue(); + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 11)) + .as("Should not match setSomeNumber with Integer input").isFalse(); assertThat(methodMatcher.matches(getAge, TestBean.class)).as("Should not match getAge").isFalse(); assertThat(methodMatcher.isRuntime()).as("Should be a runtime match").isTrue(); } @Test - public void testSimpleAdvice() { + void testSimpleAdvice() { String expression = "execution(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; CallCountingInterceptor interceptor = new CallCountingInterceptor(); TestBean testBean = getAdvisedProxy(expression, interceptor); @@ -230,7 +227,7 @@ public void testSimpleAdvice() { } @Test - public void testDynamicMatchingProxy() { + void testDynamicMatchingProxy() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number)) && args(Double)"; CallCountingInterceptor interceptor = new CallCountingInterceptor(); TestBean testBean = getAdvisedProxy(expression, interceptor); @@ -244,16 +241,15 @@ public void testDynamicMatchingProxy() { } @Test - public void testInvalidExpression() { + void testInvalidExpression() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)"; - assertThatIllegalArgumentException().isThrownBy( - getPointcut(expression)::getClassFilter); // call to getClassFilter forces resolution + assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) { TestBean target = new TestBean(); - Pointcut pointcut = getPointcut(pointcutExpression); + AspectJExpressionPointcut pointcut = getPointcut(pointcutExpression); DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(interceptor); @@ -275,44 +271,33 @@ private void assertMatchesTestBeanClass(ClassFilter classFilter) { } @Test - public void testWithUnsupportedPointcutPrimitive() { + void testWithUnsupportedPointcutPrimitive() { String expression = "call(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; - assertThatExceptionOfType(UnsupportedPointcutPrimitiveException.class).isThrownBy(() -> - getPointcut(expression).getClassFilter()) // call to getClassFilter forces resolution... - .satisfies(ex -> assertThat(ex.getUnsupportedPrimitive()).isEqualTo(PointcutPrimitive.CALL)); + assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } @Test - public void testAndSubstitution() { - Pointcut pc = getPointcut("execution(* *(..)) and args(String)"); - PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); - assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String)"); + void testAndSubstitution() { + AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String)"); + String expr = pc.getPointcutExpression().getPointcutExpression(); + assertThat(expr).isEqualTo("execution(* *(..)) && args(String)"); } @Test - public void testMultipleAndSubstitutions() { - Pointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); - PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); - assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); + void testMultipleAndSubstitutions() { + AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); + String expr = pc.getPointcutExpression().getPointcutExpression(); + assertThat(expr).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); } - private Pointcut getPointcut(String expression) { + private AspectJExpressionPointcut getPointcut(String expression) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression(expression); return pointcut; } - - public static class OtherIOther implements IOther { - - @Override - public void absquatulate() { - // Empty - } - } - @Test - public void testMatchGenericArgument() { + void testMatchGenericArgument() { String expression = "execution(* set*(java.util.List) )"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -331,7 +316,7 @@ public void testMatchGenericArgument() { } @Test - public void testMatchVarargs() throws Exception { + void testMatchVarargs() throws Exception { @SuppressWarnings("unused") class MyTemplate { @@ -357,19 +342,19 @@ public int queryForInt(String sql, Object... params) { } @Test - public void testMatchAnnotationOnClassWithAtWithin() throws Exception { + void testMatchAnnotationOnClassWithAtWithin() throws Exception { String expression = "@within(test.annotation.transaction.Tx)"; testMatchAnnotationOnClass(expression); } @Test - public void testMatchAnnotationOnClassWithoutBinding() throws Exception { + void testMatchAnnotationOnClassWithoutBinding() throws Exception { String expression = "within(@test.annotation.transaction.Tx *)"; testMatchAnnotationOnClass(expression); } @Test - public void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception { + void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception { String expression = "within(@(test.annotation..*) *)"; AspectJExpressionPointcut springAnnotatedPc = testMatchAnnotationOnClass(expression); assertThat(springAnnotatedPc.matches(TestBean.class.getMethod("setName", String.class), TestBean.class)).isFalse(); @@ -381,7 +366,7 @@ public void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception } @Test - public void testMatchAnnotationOnClassWithExactPackageWildcard() throws Exception { + void testMatchAnnotationOnClassWithExactPackageWildcard() throws Exception { String expression = "within(@(test.annotation.transaction.*) *)"; testMatchAnnotationOnClass(expression); } @@ -399,7 +384,7 @@ private AspectJExpressionPointcut testMatchAnnotationOnClass(String expression) } @Test - public void testAnnotationOnMethodWithFQN() throws Exception { + void testAnnotationOnMethodWithFQN() throws Exception { String expression = "@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -413,7 +398,7 @@ public void testAnnotationOnMethodWithFQN() throws Exception { } @Test - public void testAnnotationOnCglibProxyMethod() throws Exception { + void testAnnotationOnCglibProxyMethod() throws Exception { String expression = "@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -425,7 +410,19 @@ public void testAnnotationOnCglibProxyMethod() throws Exception { } @Test - public void testAnnotationOnDynamicProxyMethod() throws Exception { + void testNotAnnotationOnCglibProxyMethod() throws Exception { + String expression = "!@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + ProxyFactory factory = new ProxyFactory(new BeanA()); + factory.setProxyTargetClass(true); + BeanA proxy = (BeanA) factory.getProxy(); + assertThat(ajexp.matches(BeanA.class.getMethod("getAge"), proxy.getClass())).isFalse(); + } + + @Test + void testAnnotationOnDynamicProxyMethod() throws Exception { String expression = "@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -437,7 +434,19 @@ public void testAnnotationOnDynamicProxyMethod() throws Exception { } @Test - public void testAnnotationOnMethodWithWildcard() throws Exception { + void testNotAnnotationOnDynamicProxyMethod() throws Exception { + String expression = "!@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + ProxyFactory factory = new ProxyFactory(new BeanA()); + factory.setProxyTargetClass(false); + IBeanA proxy = (IBeanA) factory.getProxy(); + assertThat(ajexp.matches(IBeanA.class.getMethod("getAge"), proxy.getClass())).isFalse(); + } + + @Test + void testAnnotationOnMethodWithWildcard() throws Exception { String expression = "execution(@(test.annotation..*) * *(..))"; AspectJExpressionPointcut anySpringMethodAnnotation = new AspectJExpressionPointcut(); anySpringMethodAnnotation.setExpression(expression); @@ -453,7 +462,7 @@ public void testAnnotationOnMethodWithWildcard() throws Exception { } @Test - public void testAnnotationOnMethodArgumentsWithFQN() throws Exception { + void testAnnotationOnMethodArgumentsWithFQN() throws Exception { String expression = "@args(*, test.annotation.EmptySpringAnnotation))"; AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); takesSpringAnnotatedArgument2.setExpression(expression); @@ -482,7 +491,7 @@ public void testAnnotationOnMethodArgumentsWithFQN() throws Exception { } @Test - public void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { + void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { String expression = "execution(* *(*, @(test..*) *))"; AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); takesSpringAnnotatedArgument2.setExpression(expression); @@ -505,6 +514,15 @@ public void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { } + public static class OtherIOther implements IOther { + + @Override + public void absquatulate() { + // Empty + } + } + + public static class HasGeneric { public void setFriends(List friends) { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java index 3fd1b1e0c844..3d61e242897b 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,10 @@ * @author Ramnivas Laddad * @author Chris Beams */ -public class BeanNamePointcutMatchingTests { +class BeanNamePointcutMatchingTests { @Test - public void testMatchingPointcuts() { + void testMatchingPointcuts() { assertMatch("someName", "bean(someName)"); // Spring bean names are less restrictive compared to AspectJ names (methods, types etc.) @@ -66,7 +66,7 @@ public void testMatchingPointcuts() { } @Test - public void testNonMatchingPointcuts() { + void testNonMatchingPointcuts() { assertMisMatch("someName", "bean(someNamex)"); assertMisMatch("someName", "bean(someX*Name)"); @@ -87,7 +87,6 @@ private void assertMisMatch(String beanName, String pcExpression) { } private static boolean matches(final String beanName, String pcExpression) { - @SuppressWarnings("serial") AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut() { @Override protected String getCurrentProxiedBeanName() { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java index b465a4625142..50559b43a2e6 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.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. @@ -46,20 +46,20 @@ * @author Ramnivas Laddad * @since 2.0 */ -public class MethodInvocationProceedingJoinPointTests { +class MethodInvocationProceedingJoinPointTests { @Test - public void testingBindingWithJoinPoint() { + void testingBindingWithJoinPoint() { assertThatIllegalStateException().isThrownBy(AbstractAspectJAdvice::currentJoinPoint); } @Test - public void testingBindingWithProceedingJoinPoint() { + void testingBindingWithProceedingJoinPoint() { assertThatIllegalStateException().isThrownBy(AbstractAspectJAdvice::currentJoinPoint); } @Test - public void testCanGetMethodSignatureFromJoinPoint() { + void testCanGetMethodSignatureFromJoinPoint() { final Object raw = new TestBean(); // Will be set by advice during a method call final int newAge = 23; @@ -118,7 +118,7 @@ public void testCanGetMethodSignatureFromJoinPoint() { } @Test - public void testCanGetSourceLocationFromJoinPoint() { + void testCanGetSourceLocationFromJoinPoint() { final Object raw = new TestBean(); ProxyFactory pf = new ProxyFactory(raw); pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); @@ -135,7 +135,7 @@ public void testCanGetSourceLocationFromJoinPoint() { } @Test - public void testCanGetStaticPartFromJoinPoint() { + void testCanGetStaticPartFromJoinPoint() { final Object raw = new TestBean(); ProxyFactory pf = new ProxyFactory(raw); pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); @@ -152,7 +152,7 @@ public void testCanGetStaticPartFromJoinPoint() { } @Test - public void toShortAndLongStringFormedCorrectly() throws Exception { + void toShortAndLongStringFormedCorrectly() { final Object raw = new TestBean(); ProxyFactory pf = new ProxyFactory(raw); pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); 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 9ce2fdc69134..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 @@ -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,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; @@ -40,17 +40,17 @@ /** * @author Dave Syer */ -public class TrickyAspectJPointcutExpressionTests { +class TrickyAspectJPointcutExpressionTests { @Test - public void testManualProxyJavaWithUnconditionalPointcut() throws Exception { + void testManualProxyJavaWithUnconditionalPointcut() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); testAdvice(new DefaultPointcutAdvisor(logAdvice), logAdvice, target, "TestServiceImpl"); } @Test - public void testManualProxyJavaWithStaticPointcut() throws Exception { + void testManualProxyJavaWithStaticPointcut() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -59,7 +59,7 @@ public void testManualProxyJavaWithStaticPointcut() throws Exception { } @Test - public void testManualProxyJavaWithDynamicPointcut() throws Exception { + void testManualProxyJavaWithDynamicPointcut() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -68,7 +68,7 @@ public void testManualProxyJavaWithDynamicPointcut() throws Exception { } @Test - public void testManualProxyJavaWithDynamicPointcutAndProxyTargetClass() throws Exception { + void testManualProxyJavaWithDynamicPointcutAndProxyTargetClass() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -77,7 +77,7 @@ public void testManualProxyJavaWithDynamicPointcutAndProxyTargetClass() throws E } @Test - public void testManualProxyJavaWithStaticPointcutAndTwoClassLoaders() throws Exception { + void testManualProxyJavaWithStaticPointcutAndTwoClassLoaders() throws Exception { LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -95,13 +95,12 @@ public void testManualProxyJavaWithStaticPointcutAndTwoClassLoaders() throws Exc testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, other, "TestServiceImpl"); } - private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message) - throws Exception { + private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message) { testAdvice(advisor, logAdvice, target, message, false); } private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message, - boolean proxyTargetClass) throws Exception { + boolean proxyTargetClass) { logAdvice.reset(); @@ -148,7 +147,7 @@ public TestException(String string) { public interface TestService { - public String sayHello(); + String sayHello(); } @@ -162,14 +161,14 @@ public String sayHello() { } - public class LogUserAdvice implements MethodBeforeAdvice, ThrowsAdvice { + public static class LogUserAdvice implements MethodBeforeAdvice, ThrowsAdvice { private int countBefore = 0; private int countThrows = 0; @Override - public void before(Method method, Object[] objects, @Nullable Object o) throws Throwable { + public void before(Method method, Object[] objects, @Nullable Object o) { countBefore++; } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java index 43373063622c..0a44be6add73 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Unit tests for the {@link TypePatternClassFilter} class. + * Tests for {@link TypePatternClassFilter}. * * @author Rod Johnson * @author Rick Evans @@ -51,7 +51,7 @@ void invalidPattern() { } @Test - void invocationOfMatchesMethodBlowsUpWhenNoTypePatternHasBeenSet() throws Exception { + void invocationOfMatchesMethodBlowsUpWhenNoTypePatternHasBeenSet() { assertThatIllegalStateException().isThrownBy(() -> new TypePatternClassFilter().matches(String.class)); } 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 beb8eb0281cf..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-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. @@ -83,15 +83,15 @@ abstract class AbstractAspectJAdvisorFactoryTests { @Test void rejectsPerCflowAspect() { assertThatExceptionOfType(AopConfigException.class) - .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean"))) - .withMessageContaining("PERCFLOW"); + .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean"))) + .withMessageContaining("PERCFLOW"); } @Test void rejectsPerCflowBelowAspect() { assertThatExceptionOfType(AopConfigException.class) - .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean"))) - .withMessageContaining("PERCFLOWBELOW"); + .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean"))) + .withMessageContaining("PERCFLOWBELOW"); } @Test @@ -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,26 +342,25 @@ 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(); lockable.lock(); assertThat(lockable.locked()).as("Real target ignores locking").isTrue(); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> lockable.unlock()); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(lockable::unlock); } @Test @@ -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 } @@ -645,7 +642,7 @@ void getAge() { static class NamedPointcutAspectWithFQN { @SuppressWarnings("unused") - private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); @Around("org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.CommonPointcuts.getAge()()") int changeReturnValue(ProceedingJoinPoint pjp) { @@ -762,7 +759,7 @@ Object echo(Object obj) throws Exception { @Aspect - class DoublingAspect { + static class DoublingAspect { @Around("execution(* getAge())") public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { @@ -770,8 +767,14 @@ public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { } } + @Aspect - class IncrementingAspect extends DoublingAspect { + static class IncrementingAspect extends DoublingAspect { + + @Override + public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { + return ((int) pjp.proceed()) * 2; + } @Around("execution(* getAge())") public int incrementAge(ProceedingJoinPoint pjp) throws Throwable { @@ -780,7 +783,6 @@ public int incrementAge(ProceedingJoinPoint pjp) throws Throwable { } - @Aspect private static class InvocationTrackingAspect { @@ -807,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"); } } @@ -962,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; } @@ -1080,7 +1082,7 @@ class PerThisAspect { // Just to check that this doesn't cause problems with introduction processing @SuppressWarnings("unused") - private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); @Around("execution(int *.getAge())") int returnCountAsAge() { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java index 4b1271816898..8381a2ba16ea 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.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. @@ -42,26 +42,38 @@ */ class ArgumentBindingTests { + @Test + void annotationArgumentNameBinding() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TransactionalBean()); + proxyFactory.addAspect(PointcutWithAnnotationArgument.class); + ITransactionalBean proxiedTestBean = proxyFactory.getProxy(); + + assertThatIllegalStateException() + .isThrownBy(proxiedTestBean::doInTransaction) + .withMessage("Invoked with @Transactional"); + } + @Test void bindingInPointcutUsedByAdvice() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(NamedPointcutWithArgs.class); - ITestBean proxiedTestBean = proxyFactory.getProxy(); + assertThatIllegalArgumentException() - .isThrownBy(() -> proxiedTestBean.setName("enigma")) - .withMessage("enigma"); + .isThrownBy(() -> proxiedTestBean.setName("enigma")) + .withMessage("enigma"); } @Test - void annotationArgumentNameBinding() { - AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TransactionalBean()); - proxyFactory.addAspect(PointcutWithAnnotationArgument.class); + void bindingWithDynamicAdvice() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); + proxyFactory.addAspect(DynamicPointcutWithArgs.class); + ITestBean proxiedTestBean = proxyFactory.getProxy(); - ITransactionalBean proxiedTestBean = proxyFactory.getProxy(); - assertThatIllegalStateException() - .isThrownBy(proxiedTestBean::doInTransaction) - .withMessage("Invoked with @Transactional"); + proxiedTestBean.applyName(1); + assertThatIllegalArgumentException() + .isThrownBy(() -> proxiedTestBean.applyName("enigma")) + .withMessage("enigma"); } @Test @@ -94,6 +106,7 @@ public void doInTransaction() { } } + /** * Mimics Spring's @Transactional annotation without actually introducing the dependency. */ @@ -101,16 +114,17 @@ public void doInTransaction() { @interface Transactional { } + @Aspect static class PointcutWithAnnotationArgument { - @Around(value = "execution(* org.springframework..*.*(..)) && @annotation(transactional)") - public Object around(ProceedingJoinPoint pjp, Transactional transactional) throws Throwable { + @Around("execution(* org.springframework..*.*(..)) && @annotation(transactional)") + public Object around(ProceedingJoinPoint pjp, Transactional transactional) { throw new IllegalStateException("Invoked with @Transactional"); } - } + @Aspect static class NamedPointcutWithArgs { @@ -118,10 +132,19 @@ static class NamedPointcutWithArgs { public void pointcutWithArgs(String s) {} @Around("pointcutWithArgs(aString)") - public Object doAround(ProceedingJoinPoint pjp, String aString) throws Throwable { + public Object doAround(ProceedingJoinPoint pjp, String aString) { throw new IllegalArgumentException(aString); } + } + + @Aspect("pertarget(execution(* *(..)))") + static class DynamicPointcutWithArgs { + + @Around("execution(* *(..)) && args(java.lang.String)") + public Object doAround(ProceedingJoinPoint pjp) { + throw new IllegalArgumentException(String.valueOf(pjp.getArgs()[0])); + } } } 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 new file mode 100644 index 000000000000..446b9bc34956 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java @@ -0,0 +1,84 @@ +/* + * 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.aop.aspectj.annotation; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; + +/** + * Tests for {@link AspectJAdvisorBeanRegistrationAotProcessor}. + * + * @author Sebastien Deleuze + * @since 6.1 + */ +class AspectJAdvisorBeanRegistrationAotProcessorTests { + + private final GenerationContext generationContext = new TestGenerationContext(); + + private final RuntimeHints runtimeHints = this.generationContext.getRuntimeHints(); + + + @Test + void shouldProcessAspectJClass() { + process(AspectJClass.class); + assertThat(reflection().onType(AspectJClass.class).withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)) + .accepts(this.runtimeHints); + } + + @Test + void shouldSkipRegularClass() { + process(RegularClass.class); + assertThat(this.runtimeHints.reflection().typeHints()).isEmpty(); + } + + void process(Class beanClass) { + BeanRegistrationAotContribution contribution = createContribution(beanClass); + if (contribution != null) { + contribution.applyTo(this.generationContext, mock()); + } + } + + private static @Nullable BeanRegistrationAotContribution createContribution(Class beanClass) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); + return new AspectJAdvisorBeanRegistrationAotProcessor() + .processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.getName())); + } + + + static class AspectJClass { + private static java.lang.Throwable ajc$initFailureCause; + } + + static class RegularClass { + private static java.lang.Throwable initFailureCause; + } + +} 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/aspectj/annotation/AspectJPointcutAdvisorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.java index 15203879aad2..79a67904814b 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.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,9 +20,9 @@ import org.springframework.aop.Pointcut; import org.springframework.aop.aspectj.AspectJExpressionPointcut; -import org.springframework.aop.aspectj.AspectJExpressionPointcutTests; import org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.ExceptionThrowingAspect; import org.springframework.aop.framework.AopConfigException; +import org.springframework.aop.testfixture.aspectj.CommonExpressions; import org.springframework.aop.testfixture.aspectj.PerTargetAspect; import org.springframework.beans.testfixture.beans.TestBean; @@ -33,15 +33,15 @@ * @author Rod Johnson * @author Chris Beams */ -public class AspectJPointcutAdvisorTests { +class AspectJPointcutAdvisorTests { private final AspectJAdvisorFactory af = new ReflectiveAspectJAdvisorFactory(); @Test - public void testSingleton() throws SecurityException, NoSuchMethodException { + void testSingleton() throws SecurityException, NoSuchMethodException { AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); - ajexp.setExpression(AspectJExpressionPointcutTests.MATCH_ALL_METHODS); + ajexp.setExpression(CommonExpressions.MATCH_ALL_METHODS); InstantiationModelAwarePointcutAdvisorImpl ajpa = new InstantiationModelAwarePointcutAdvisorImpl( ajexp, TestBean.class.getMethod("getAge"), af, @@ -53,9 +53,9 @@ public void testSingleton() throws SecurityException, NoSuchMethodException { } @Test - public void testPerTarget() throws SecurityException, NoSuchMethodException { + void testPerTarget() throws SecurityException, NoSuchMethodException { AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); - ajexp.setExpression(AspectJExpressionPointcutTests.MATCH_ALL_METHODS); + ajexp.setExpression(CommonExpressions.MATCH_ALL_METHODS); InstantiationModelAwarePointcutAdvisorImpl ajpa = new InstantiationModelAwarePointcutAdvisorImpl( ajexp, TestBean.class.getMethod("getAge"), af, @@ -76,13 +76,13 @@ public void testPerTarget() throws SecurityException, NoSuchMethodException { } @Test - public void testPerCflowTarget() { + void testPerCflowTarget() { assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> testIllegalInstantiationModel(AbstractAspectJAdvisorFactoryTests.PerCflowAspect.class)); } @Test - public void testPerCflowBelowTarget() { + void testPerCflowBelowTarget() { assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> testIllegalInstantiationModel(AbstractAspectJAdvisorFactoryTests.PerCflowBelowAspect.class)); } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java index 3fb1b05d81b9..9e45538c713f 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.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. @@ -36,17 +36,16 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class AspectProxyFactoryTests { +class AspectProxyFactoryTests { @Test - public void testWithNonAspect() { + void testWithNonAspect() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); - assertThatIllegalArgumentException().isThrownBy(() -> - proxyFactory.addAspect(TestBean.class)); + assertThatIllegalArgumentException().isThrownBy(() -> proxyFactory.addAspect(TestBean.class)); } @Test - public void testWithSimpleAspect() throws Exception { + void testWithSimpleAspect() { TestBean bean = new TestBean(); bean.setAge(2); AspectJProxyFactory proxyFactory = new AspectJProxyFactory(bean); @@ -56,7 +55,7 @@ public void testWithSimpleAspect() throws Exception { } @Test - public void testWithPerThisAspect() throws Exception { + void testWithPerThisAspect() { TestBean bean1 = new TestBean(); TestBean bean2 = new TestBean(); @@ -76,15 +75,14 @@ public void testWithPerThisAspect() throws Exception { } @Test - public void testWithInstanceWithNonAspect() throws Exception { + void testWithInstanceWithNonAspect() { AspectJProxyFactory pf = new AspectJProxyFactory(); - assertThatIllegalArgumentException().isThrownBy(() -> - pf.addAspect(new TestBean())); + assertThatIllegalArgumentException().isThrownBy(() -> pf.addAspect(new TestBean())); } @Test @SuppressWarnings("unchecked") - public void testSerializable() throws Exception { + void testSerializable() throws Exception { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(LoggingAspectOnVarargs.class); ITestBean proxy = proxyFactory.getProxy(); @@ -94,7 +92,7 @@ public void testSerializable() throws Exception { } @Test - public void testWithInstance() throws Exception { + void testWithInstance() throws Exception { MultiplyReturnValue aspect = new MultiplyReturnValue(); int multiple = 3; aspect.setMultiple(multiple); @@ -113,14 +111,14 @@ public void testWithInstance() throws Exception { } @Test - public void testWithNonSingletonAspectInstance() throws Exception { + void testWithNonSingletonAspectInstance() { AspectJProxyFactory pf = new AspectJProxyFactory(); assertThatIllegalArgumentException().isThrownBy(() -> pf.addAspect(new PerThisAspect())); } @Test // SPR-13328 @SuppressWarnings("unchecked") - public void testProxiedVarargsWithEnumArray() throws Exception { + public void testProxiedVarargsWithEnumArray() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(LoggingAspectOnVarargs.class); ITestBean proxy = proxyFactory.getProxy(); @@ -129,7 +127,7 @@ public void testProxiedVarargsWithEnumArray() throws Exception { @Test // SPR-13328 @SuppressWarnings("unchecked") - public void testUnproxiedVarargsWithEnumArray() throws Exception { + public void testUnproxiedVarargsWithEnumArray() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(LoggingAspectOnSetter.class); ITestBean proxy = proxyFactory.getProxy(); @@ -174,13 +172,13 @@ public interface MyInterface { public enum MyEnum implements MyInterface { - A, B; + A, B } public enum MyOtherEnum implements MyInterface { - C, D; + C, D } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java index a6ecf37304de..f12c6b982a43 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -37,7 +37,7 @@ * @author Rob Harrop * @author Chris Beams */ -public class AspectJNamespaceHandlerTests { +class AspectJNamespaceHandlerTests { private ParserContext parserContext; @@ -47,7 +47,7 @@ public class AspectJNamespaceHandlerTests { @BeforeEach - public void setUp() throws Exception { + public void setUp() { SourceExtractor sourceExtractor = new PassThroughSourceExtractor(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.registry); XmlReaderContext readerContext = @@ -56,7 +56,7 @@ public void setUp() throws Exception { } @Test - public void testRegisterAutoProxyCreator() throws Exception { + void testRegisterAutoProxyCreator() { AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); @@ -65,7 +65,7 @@ public void testRegisterAutoProxyCreator() throws Exception { } @Test - public void testRegisterAspectJAutoProxyCreator() throws Exception { + void testRegisterAspectJAutoProxyCreator() { AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); @@ -77,7 +77,7 @@ public void testRegisterAspectJAutoProxyCreator() throws Exception { } @Test - public void testRegisterAspectJAutoProxyCreatorWithExistingAutoProxyCreator() throws Exception { + void testRegisterAspectJAutoProxyCreatorWithExistingAutoProxyCreator() { AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).isEqualTo(1); @@ -89,7 +89,7 @@ public void testRegisterAspectJAutoProxyCreatorWithExistingAutoProxyCreator() th } @Test - public void testRegisterAutoProxyCreatorWhenAspectJAutoProxyCreatorAlreadyExists() throws Exception { + void testRegisterAutoProxyCreatorWhenAspectJAutoProxyCreatorAlreadyExists() { AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).isEqualTo(1); diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java index d391d45f39bd..118828f338b4 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -40,7 +40,7 @@ * @author Adrian Colyer * @author Chris Beams */ -public class AspectJPrecedenceComparatorTests { +class AspectJPrecedenceComparatorTests { private static final int HIGH_PRECEDENCE_ADVISOR_ORDER = 100; private static final int LOW_PRECEDENCE_ADVISOR_ORDER = 200; @@ -56,7 +56,7 @@ public class AspectJPrecedenceComparatorTests { @BeforeEach - public void setUp() throws Exception { + public void setUp() { this.comparator = new AspectJPrecedenceComparator(); this.anyOldMethod = getClass().getMethods()[0]; this.anyOldPointcut = new AspectJExpressionPointcut(); @@ -65,7 +65,7 @@ public void setUp() throws Exception { @Test - public void testSameAspectNoAfterAdvice() { + void testSameAspectNoAfterAdvice() { Advisor advisor1 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); @@ -76,7 +76,7 @@ public void testSameAspectNoAfterAdvice() { } @Test - public void testSameAspectAfterAdvice() { + void testSameAspectAfterAdvice() { Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor2 sorted before advisor1").isEqualTo(1); @@ -87,14 +87,14 @@ public void testSameAspectAfterAdvice() { } @Test - public void testSameAspectOneOfEach() { + void testSameAspectOneOfEach() { Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 and advisor2 not comparable").isEqualTo(1); } @Test - public void testSameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { + void testSameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { Advisor advisor1 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("nothing to say about order here").isEqualTo(0); @@ -105,7 +105,7 @@ public void testSameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { } @Test - public void testSameAdvisorPrecedenceDifferentAspectAfterAdvice() { + void testSameAdvisorPrecedenceDifferentAspectAfterAdvice() { Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("nothing to say about order here").isEqualTo(0); @@ -116,7 +116,7 @@ public void testSameAdvisorPrecedenceDifferentAspectAfterAdvice() { } @Test - public void testHigherAdvisorPrecedenceNoAfterAdvice() { + void testHigherAdvisorPrecedenceNoAfterAdvice() { Advisor advisor1 = createSpringAOPBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER); Advisor advisor2 = createAspectJBeforeAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); @@ -127,7 +127,7 @@ public void testHigherAdvisorPrecedenceNoAfterAdvice() { } @Test - public void testHigherAdvisorPrecedenceAfterAdvice() { + void testHigherAdvisorPrecedenceAfterAdvice() { Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJAroundAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); @@ -138,7 +138,7 @@ public void testHigherAdvisorPrecedenceAfterAdvice() { } @Test - public void testLowerAdvisorPrecedenceNoAfterAdvice() { + void testLowerAdvisorPrecedenceNoAfterAdvice() { Advisor advisor1 = createAspectJBeforeAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted after advisor2").isEqualTo(1); @@ -149,7 +149,7 @@ public void testLowerAdvisorPrecedenceNoAfterAdvice() { } @Test - public void testLowerAdvisorPrecedenceAfterAdvice() { + void testLowerAdvisorPrecedenceAfterAdvice() { Advisor advisor1 = createAspectJAfterAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); Advisor advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted after advisor2").isEqualTo(1); 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 new file mode 100644 index 000000000000..f25b17a10b93 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/AbstractProxyExceptionHandlingTests.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2025 the original author 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.UndeclaredThrowableException; +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 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; + +/** + * @author Mikaël Francoeur + * @author Sam Brannen + * @since 6.2 + * @see JdkProxyExceptionHandlingTests + * @see CglibProxyExceptionHandlingTests + */ +@IndicativeSentencesGeneration(generator = SentenceFragmentDisplayNameGenerator.class) +abstract class AbstractProxyExceptionHandlingTests { + + private static final RuntimeException uncheckedException = new RuntimeException(); + + private static final DeclaredCheckedException declaredCheckedException = new DeclaredCheckedException(); + + private static final UndeclaredCheckedException undeclaredCheckedException = new UndeclaredCheckedException(); + + protected final MyClass target = mock(); + + protected final ProxyFactory proxyFactory = new ProxyFactory(target); + + protected MyInterface proxy; + + private Throwable throwableSeenByCaller; + + + @BeforeEach + void clear() { + Mockito.clearInvocations(target); + } + + + protected abstract void assertProxyType(Object proxy); + + + private void invokeProxy() { + throwableSeenByCaller = catchThrowable(() -> Objects.requireNonNull(proxy).doSomething()); + } + + @SuppressWarnings("SameParameterValue") + private static Answer sneakyThrow(Throwable throwable) { + return invocation -> { + throw throwable; + }; + } + + + @Nested + @SentenceFragment("when there is one interceptor") + class WhenThereIsOneInterceptorTests { + + private @Nullable Throwable throwableSeenByInterceptor; + + @BeforeEach + void beforeEach() { + proxyFactory.addAdvice(captureThrowable()); + proxy = (MyInterface) proxyFactory.getProxy(getClass().getClassLoader()); + assertProxyType(proxy); + } + + @Test + @SentenceFragment("and the target throws an undeclared checked exception") + void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException { + willAnswer(sneakyThrow(undeclaredCheckedException)).given(target).doSomething(); + invokeProxy(); + assertThat(throwableSeenByInterceptor).isSameAs(undeclaredCheckedException); + assertThat(throwableSeenByCaller) + .isInstanceOf(UndeclaredThrowableException.class) + .cause().isSameAs(undeclaredCheckedException); + } + + @Test + @SentenceFragment("and the target throws a declared checked exception") + void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException { + willThrow(declaredCheckedException).given(target).doSomething(); + invokeProxy(); + assertThat(throwableSeenByInterceptor).isSameAs(declaredCheckedException); + assertThat(throwableSeenByCaller).isSameAs(declaredCheckedException); + } + + @Test + @SentenceFragment("and the target throws an unchecked exception") + void targetThrowsUncheckedException() throws DeclaredCheckedException { + willThrow(uncheckedException).given(target).doSomething(); + invokeProxy(); + assertThat(throwableSeenByInterceptor).isSameAs(uncheckedException); + assertThat(throwableSeenByCaller).isSameAs(uncheckedException); + } + + private MethodInterceptor captureThrowable() { + return invocation -> { + try { + return invocation.proceed(); + } + catch (Exception ex) { + throwableSeenByInterceptor = ex; + throw ex; + } + }; + } + } + + + @Nested + @SentenceFragment("when there are no interceptors") + class WhenThereAreNoInterceptorsTests { + + @BeforeEach + void beforeEach() { + proxy = (MyInterface) proxyFactory.getProxy(getClass().getClassLoader()); + assertProxyType(proxy); + } + + @Test + @SentenceFragment("and the target throws an undeclared checked exception") + void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException { + willAnswer(sneakyThrow(undeclaredCheckedException)).given(target).doSomething(); + invokeProxy(); + assertThat(throwableSeenByCaller) + .isInstanceOf(UndeclaredThrowableException.class) + .cause().isSameAs(undeclaredCheckedException); + } + + @Test + @SentenceFragment("and the target throws a declared checked exception") + void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException { + willThrow(declaredCheckedException).given(target).doSomething(); + invokeProxy(); + assertThat(throwableSeenByCaller).isSameAs(declaredCheckedException); + } + + @Test + @SentenceFragment("and the target throws an unchecked exception") + void targetThrowsUncheckedException() throws DeclaredCheckedException { + willThrow(uncheckedException).given(target).doSomething(); + invokeProxy(); + assertThat(throwableSeenByCaller).isSameAs(uncheckedException); + } + } + + + interface MyInterface { + + void doSomething() throws DeclaredCheckedException; + } + + static class MyClass implements MyInterface { + + @Override + public void doSomething() throws DeclaredCheckedException { + throw declaredCheckedException; + } + } + + @SuppressWarnings("serial") + private static class UndeclaredCheckedException extends Exception { + } + + @SuppressWarnings("serial") + private static class DeclaredCheckedException extends Exception { + } + +} 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 new file mode 100644 index 000000000000..2de692498041 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/CglibProxyExceptionHandlingTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2025 the original author 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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; + +import org.springframework.cglib.proxy.Enhancer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mikaël Francoeur + * @since 6.2 + * @see JdkProxyExceptionHandlingTests + */ +@DisplayName("CGLIB proxy exception handling") +class CglibProxyExceptionHandlingTests extends AbstractProxyExceptionHandlingTests { + + @BeforeEach + void setup() { + proxyFactory.setProxyTargetClass(true); + } + + @Override + protected void assertProxyType(Object proxy) { + assertThat(Enhancer.isEnhanced(proxy.getClass())).isTrue(); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/ClassWithConstructor.java b/spring-aop/src/test/java/org/springframework/aop/framework/ClassWithConstructor.java deleted file mode 100644 index dfbd090e8da6..000000000000 --- a/spring-aop/src/test/java/org/springframework/aop/framework/ClassWithConstructor.java +++ /dev/null @@ -1,28 +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.aop.framework; - -public class ClassWithConstructor { - - public ClassWithConstructor(Object object) { - - } - - public void method() { - - } -} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java index 959c4cd25a30..33326d426a6d 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.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. @@ -25,14 +25,14 @@ /** * Benchmarks for introductions. - * + *

* NOTE: No assertions! * * @author Rod Johnson * @author Chris Beams * @since 2.0 */ -public class IntroductionBenchmarkTests { +class IntroductionBenchmarkTests { private static final int EXPECTED_COMPARE = 13; @@ -53,7 +53,7 @@ public interface Counter { } @Test - public void timeManyInvocations() { + void timeManyInvocations() { StopWatch sw = new StopWatch(); TestBean target = new TestBean(); 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 new file mode 100644 index 000000000000..a7bae1a7c9e0 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/JdkProxyExceptionHandlingTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2025 the original author 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.Proxy; + +import org.junit.jupiter.api.DisplayName; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mikaël Francoeur + * @since 6.2 + * @see CglibProxyExceptionHandlingTests + */ +@DisplayName("JDK proxy exception handling") +class JdkProxyExceptionHandlingTests extends AbstractProxyExceptionHandlingTests { + + @Override + protected void assertProxyType(Object proxy) { + assertThat(Proxy.isProxyClass(proxy.getClass())).isTrue(); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java index 047e84b3d72f..abf36f2adda9 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,14 +29,14 @@ * * @author Dave Syer */ -public class NullPrimitiveTests { +class NullPrimitiveTests { interface Foo { int getValue(); } @Test - public void testNullPrimitiveWithJdkProxy() { + void testNullPrimitiveWithJdkProxy() { class SimpleFoo implements Foo { @Override @@ -51,8 +51,7 @@ public int getValue() { Foo foo = (Foo) factory.getProxy(); - assertThatExceptionOfType(AopInvocationException.class).isThrownBy(() -> - foo.getValue()) + assertThatExceptionOfType(AopInvocationException.class).isThrownBy(foo::getValue) .withMessageContaining("Foo.getValue()"); } @@ -63,7 +62,7 @@ public int getValue() { } @Test - public void testNullPrimitiveWithCglibProxy() { + void testNullPrimitiveWithCglibProxy() { Bar target = new Bar(); ProxyFactory factory = new ProxyFactory(target); @@ -71,8 +70,7 @@ public void testNullPrimitiveWithCglibProxy() { Bar bar = (Bar) factory.getProxy(); - assertThatExceptionOfType(AopInvocationException.class).isThrownBy(() -> - bar.getValue()) + assertThatExceptionOfType(AopInvocationException.class).isThrownBy(bar::getValue) .withMessageContaining("Bar.getValue()"); } diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java index d58a9b594a16..826191596593 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -32,13 +32,13 @@ * @author Chris Beams * @since 03.09.2004 */ -public class PrototypeTargetTests { +class PrototypeTargetTests { private static final Resource CONTEXT = qualifiedResource(PrototypeTargetTests.class, "context.xml"); @Test - public void testPrototypeProxyWithPrototypeTarget() { + void testPrototypeProxyWithPrototypeTarget() { TestBeanImpl.constructionCount = 0; DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); @@ -52,7 +52,7 @@ public void testPrototypeProxyWithPrototypeTarget() { } @Test - public void testSingletonProxyWithPrototypeTarget() { + void testSingletonProxyWithPrototypeTarget() { TestBeanImpl.constructionCount = 0; DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java index a0f52916504a..476681fee264 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.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,16 +19,10 @@ import java.sql.SQLException; import java.sql.Savepoint; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import javax.accessibility.Accessible; -import javax.swing.JFrame; -import javax.swing.RootPaneContainer; - import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.aop.Advisor; @@ -57,10 +51,10 @@ * @author Chris Beams * @since 14.05.2003 */ -public class ProxyFactoryTests { +class ProxyFactoryTests { @Test - public void testIndexOfMethods() { + void indexOfMethods() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -76,7 +70,7 @@ public void testIndexOfMethods() { } @Test - public void testRemoveAdvisorByReference() { + void removeAdvisorByReference() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -96,7 +90,7 @@ public void testRemoveAdvisorByReference() { } @Test - public void testRemoveAdvisorByIndex() { + void removeAdvisorByIndex() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -144,7 +138,7 @@ public void testRemoveAdvisorByIndex() { } @Test - public void testReplaceAdvisor() { + void replaceAdvisor() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -173,7 +167,7 @@ public void testReplaceAdvisor() { } @Test - public void testAddRepeatedInterface() { + void addRepeatedInterface() { TimeStamped tst = () -> { throw new UnsupportedOperationException("getTimeStamp"); }; @@ -186,7 +180,7 @@ public void testAddRepeatedInterface() { } @Test - public void testGetsAllInterfaces() { + void getsAllInterfaces() { // Extend to get new interface class TestBeanSubclass extends TestBean implements Comparable { @Override @@ -195,61 +189,58 @@ public int compareTo(Object arg0) { } } TestBeanSubclass raw = new TestBeanSubclass(); - ProxyFactory factory = new ProxyFactory(raw); - //System.out.println("Proxied interfaces are " + StringUtils.arrayToDelimitedString(factory.getProxiedInterfaces(), ",")); - assertThat(factory.getProxiedInterfaces()).as("Found correct number of interfaces").hasSize(5); - ITestBean tb = (ITestBean) factory.getProxy(); + ProxyFactory pf = new ProxyFactory(raw); + assertThat(pf.getProxiedInterfaces()).as("Found correct number of interfaces").hasSize(5); + ITestBean tb = (ITestBean) pf.getProxy(); assertThat(tb).as("Picked up secondary interface").isInstanceOf(IOther.class); raw.setAge(25); assertThat(tb.getAge()).isEqualTo(raw.getAge()); + Class[] oldProxiedInterfaces = pf.getProxiedInterfaces(); long t = 555555L; TimestampIntroductionInterceptor ti = new TimestampIntroductionInterceptor(t); + pf.addAdvisor(new DefaultIntroductionAdvisor(ti, TimeStamped.class)); - Class[] oldProxiedInterfaces = factory.getProxiedInterfaces(); - - factory.addAdvisor(0, new DefaultIntroductionAdvisor(ti, TimeStamped.class)); - - Class[] newProxiedInterfaces = factory.getProxiedInterfaces(); + Class[] newProxiedInterfaces = pf.getProxiedInterfaces(); assertThat(newProxiedInterfaces).as("Advisor proxies one more interface after introduction").hasSize(oldProxiedInterfaces.length + 1); - TimeStamped ts = (TimeStamped) factory.getProxy(); + TimeStamped ts = (TimeStamped) pf.getProxy(); assertThat(ts.getTimeStamp()).isEqualTo(t); // Shouldn't fail; ((IOther) ts).absquatulate(); } @Test - public void testInterceptorInclusionMethods() { + void interceptorInclusionMethods() { class MyInterceptor implements MethodInterceptor { @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public Object invoke(MethodInvocation invocation) { throw new UnsupportedOperationException(); } } NopInterceptor di = new NopInterceptor(); NopInterceptor diUnused = new NopInterceptor(); - ProxyFactory factory = new ProxyFactory(new TestBean()); - factory.addAdvice(0, di); - assertThat(factory.getProxy()).isInstanceOf(ITestBean.class); - assertThat(factory.adviceIncluded(di)).isTrue(); - assertThat(factory.adviceIncluded(diUnused)).isFalse(); - assertThat(factory.countAdvicesOfType(NopInterceptor.class)).isEqualTo(1); - assertThat(factory.countAdvicesOfType(MyInterceptor.class)).isEqualTo(0); - - factory.addAdvice(0, diUnused); - assertThat(factory.adviceIncluded(diUnused)).isTrue(); - assertThat(factory.countAdvicesOfType(NopInterceptor.class)).isEqualTo(2); + ProxyFactory pf = new ProxyFactory(new TestBean()); + pf.addAdvice(0, di); + assertThat(pf.getProxy()).isInstanceOf(ITestBean.class); + assertThat(pf.adviceIncluded(di)).isTrue(); + assertThat(pf.adviceIncluded(diUnused)).isFalse(); + assertThat(pf.countAdvicesOfType(NopInterceptor.class)).isEqualTo(1); + assertThat(pf.countAdvicesOfType(MyInterceptor.class)).isEqualTo(0); + + pf.addAdvice(0, diUnused); + assertThat(pf.adviceIncluded(diUnused)).isTrue(); + assertThat(pf.countAdvicesOfType(NopInterceptor.class)).isEqualTo(2); } @Test - public void testSealedInterfaceExclusion() { + void sealedInterfaceExclusion() { // String implements ConstantDesc on JDK 12+, sealed as of JDK 17 - ProxyFactory factory = new ProxyFactory(new String()); + ProxyFactory pf = new ProxyFactory(""); NopInterceptor di = new NopInterceptor(); - factory.addAdvice(0, di); - Object proxy = factory.getProxy(); + pf.addAdvice(0, di); + Object proxy = pf.getProxy(); assertThat(proxy).isInstanceOf(CharSequence.class); } @@ -257,7 +248,7 @@ public void testSealedInterfaceExclusion() { * Should see effect immediately on behavior. */ @Test - public void testCanAddAndRemoveAspectInterfacesOnSingleton() { + void canAddAndRemoveAspectInterfacesOnSingleton() { ProxyFactory config = new ProxyFactory(new TestBean()); assertThat(config.getProxy()).as("Shouldn't implement TimeStamped before manipulation") @@ -304,7 +295,7 @@ public void testCanAddAndRemoveAspectInterfacesOnSingleton() { } @Test - public void testProxyTargetClassWithInterfaceAsTarget() { + void proxyTargetClassWithInterfaceAsTarget() { ProxyFactory pf = new ProxyFactory(); pf.setTargetClass(ITestBean.class); Object proxy = pf.getProxy(); @@ -320,7 +311,7 @@ public void testProxyTargetClassWithInterfaceAsTarget() { } @Test - public void testProxyTargetClassWithConcreteClassAsTarget() { + void proxyTargetClassWithConcreteClassAsTarget() { ProxyFactory pf = new ProxyFactory(); pf.setTargetClass(TestBean.class); Object proxy = pf.getProxy(); @@ -337,29 +328,57 @@ public void testProxyTargetClassWithConcreteClassAsTarget() { } @Test - @Disabled("Not implemented yet, see https://jira.springframework.org/browse/SPR-5708") - public void testExclusionOfNonPublicInterfaces() { - JFrame frame = new JFrame(); - ProxyFactory proxyFactory = new ProxyFactory(frame); - Object proxy = proxyFactory.getProxy(); - assertThat(proxy).isInstanceOf(RootPaneContainer.class); - assertThat(proxy).isInstanceOf(Accessible.class); + void proxyTargetClassInCaseOfIntroducedInterface() { + ProxyFactory pf = new ProxyFactory(); + pf.setTargetClass(MyDate.class); + TimestampIntroductionInterceptor ti = new TimestampIntroductionInterceptor(0L); + pf.addAdvisor(new DefaultIntroductionAdvisor(ti, TimeStamped.class)); + Object proxy = pf.getProxy(); + assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue(); + assertThat(proxy).isInstanceOf(MyDate.class); + assertThat(proxy).isInstanceOf(TimeStamped.class); + assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(MyDate.class); + } + + @Test + void proxyInterfaceInCaseOfIntroducedInterfaceOnly() { + ProxyFactory pf = new ProxyFactory(); + pf.addInterface(TimeStamped.class); + TimestampIntroductionInterceptor ti = new TimestampIntroductionInterceptor(0L); + pf.addAdvisor(new DefaultIntroductionAdvisor(ti, TimeStamped.class)); + Object proxy = pf.getProxy(); + assertThat(AopUtils.isJdkDynamicProxy(proxy)).as("Proxy is a JDK proxy").isTrue(); + assertThat(proxy).isInstanceOf(TimeStamped.class); + assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(proxy.getClass()); + } + + @Test + void proxyInterfaceInCaseOfNonTargetInterface() { + ProxyFactory pf = new ProxyFactory(); + pf.setTargetClass(MyDate.class); + pf.addInterface(TimeStamped.class); + pf.addAdvice((MethodInterceptor) invocation -> { + throw new UnsupportedOperationException(); + }); + Object proxy = pf.getProxy(); + assertThat(AopUtils.isJdkDynamicProxy(proxy)).as("Proxy is a JDK proxy").isTrue(); + assertThat(proxy).isInstanceOf(TimeStamped.class); + assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(MyDate.class); } @Test - public void testInterfaceProxiesCanBeOrderedThroughAnnotations() { + void interfaceProxiesCanBeOrderedThroughAnnotations() { Object proxy1 = new ProxyFactory(new A()).getProxy(); Object proxy2 = new ProxyFactory(new B()).getProxy(); List list = new ArrayList<>(2); list.add(proxy1); list.add(proxy2); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isSameAs(proxy2); - assertThat(list.get(1)).isSameAs(proxy1); + assertThat(list).containsExactly(proxy2, proxy1); } @Test - public void testTargetClassProxiesCanBeOrderedThroughAnnotations() { + void targetClassProxiesCanBeOrderedThroughAnnotations() { ProxyFactory pf1 = new ProxyFactory(new A()); pf1.setProxyTargetClass(true); ProxyFactory pf2 = new ProxyFactory(new B()); @@ -370,12 +389,11 @@ public void testTargetClassProxiesCanBeOrderedThroughAnnotations() { list.add(proxy1); list.add(proxy2); AnnotationAwareOrderComparator.sort(list); - assertThat(list.get(0)).isSameAs(proxy2); - assertThat(list.get(1)).isSameAs(proxy1); + assertThat(list).containsExactly(proxy2, proxy1); } @Test - public void testInterceptorWithoutJoinpoint() { + void interceptorWithoutJoinpoint() { final TestBean target = new TestBean("tb"); ITestBean proxy = ProxyFactory.getProxy(ITestBean.class, (MethodInterceptor) invocation -> { assertThat(invocation.getThis()).isNull(); @@ -385,35 +403,35 @@ public void testInterceptorWithoutJoinpoint() { } @Test - public void testCharSequenceProxy() { + void interfaceProxy() { CharSequence target = "test"; ProxyFactory pf = new ProxyFactory(target); ClassLoader cl = target.getClass().getClassLoader(); CharSequence proxy = (CharSequence) pf.getProxy(cl); - assertThat(proxy.toString()).isEqualTo(target); + assertThat(proxy).asString().isEqualTo(target); assertThat(pf.getProxyClass(cl)).isSameAs(proxy.getClass()); } @Test - public void testDateProxy() { - Date target = new Date(); + void dateProxy() { + MyDate target = new MyDate(); ProxyFactory pf = new ProxyFactory(target); pf.setProxyTargetClass(true); ClassLoader cl = target.getClass().getClassLoader(); - Date proxy = (Date) pf.getProxy(cl); + MyDate proxy = (MyDate) pf.getProxy(cl); assertThat(proxy.getTime()).isEqualTo(target.getTime()); assertThat(pf.getProxyClass(cl)).isSameAs(proxy.getClass()); } @Test - public void testJdbcSavepointProxy() throws SQLException { + void jdbcSavepointProxy() throws SQLException { Savepoint target = new Savepoint() { @Override - public int getSavepointId() throws SQLException { + public int getSavepointId() { return 1; } @Override - public String getSavepointName() throws SQLException { + public String getSavepointName() { return "sp"; } }; @@ -425,8 +443,20 @@ public String getSavepointName() throws SQLException { } + // Emulates java.util.Date locally, since we cannot automatically proxy the + // java.util.Date class. + static class MyDate { + + private final long time = System.currentTimeMillis(); + + public long getTime() { + return time; + } + } + + @Order(2) - public static class A implements Runnable { + static class A implements Runnable { @Override public void run() { @@ -435,7 +465,7 @@ public void run() { @Order(1) - public static class B implements Runnable { + static class B implements Runnable { @Override public void run() { 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/framework/adapter/ThrowsAdviceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java index e1b22b43bdcc..1a669c018e0d 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.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. @@ -37,17 +37,17 @@ * @author Chris Beams * @author Juergen Hoeller */ -public class ThrowsAdviceInterceptorTests { +class ThrowsAdviceInterceptorTests { @Test - public void testNoHandlerMethods() { + void testNoHandlerMethods() { // should require one handler method at least assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> new ThrowsAdviceInterceptor(new Object())); } @Test - public void testNotInvoked() throws Throwable { + void testNotInvoked() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); Object ret = new Object(); @@ -58,7 +58,7 @@ public void testNotInvoked() throws Throwable { } @Test - public void testNoHandlerMethodForThrowable() throws Throwable { + void testNoHandlerMethodForThrowable() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); assertThat(ti.getHandlerMethodCount()).isEqualTo(2); @@ -70,7 +70,7 @@ public void testNoHandlerMethodForThrowable() throws Throwable { } @Test - public void testCorrectHandlerUsed() throws Throwable { + void testCorrectHandlerUsed() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); FileNotFoundException ex = new FileNotFoundException(); @@ -84,7 +84,7 @@ public void testCorrectHandlerUsed() throws Throwable { } @Test - public void testCorrectHandlerUsedForSubclass() throws Throwable { + void testCorrectHandlerUsedForSubclass() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); // Extends RemoteException @@ -97,10 +97,9 @@ public void testCorrectHandlerUsedForSubclass() throws Throwable { } @Test - public void testHandlerMethodThrowsException() throws Throwable { + void testHandlerMethodThrowsException() throws Throwable { final Throwable t = new Throwable(); - @SuppressWarnings("serial") MyThrowsHandler th = new MyThrowsHandler() { @Override public void afterThrowing(RemoteException ex) throws Throwable { 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 new file mode 100644 index 000000000000..92c295a59ecb --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2025 the original author 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.interceptor; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +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.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AsyncExecutionInterceptor}. + * + * @author Bao Ngo + * @since 7.0 + */ +class AsyncExecutionInterceptorTests { + + @Test + @SuppressWarnings("unchecked") + void invokeOnInterfaceWithGeneric() throws Throwable { + AsyncExecutionInterceptor interceptor = spy(new AsyncExecutionInterceptor(null)); + FutureRunner impl = new FutureRunner(); + MethodInvocation mi = mock(); + given(mi.getThis()).willReturn(impl); + given(mi.getMethod()).willReturn(GenericRunner.class.getMethod("run")); + + interceptor.invoke(mi); + ArgumentCaptor> classArgumentCaptor = ArgumentCaptor.forClass(Class.class); + verify(interceptor).doSubmit(any(Callable.class), any(AsyncTaskExecutor.class), classArgumentCaptor.capture()); + assertThat(classArgumentCaptor.getValue()).isEqualTo(Future.class); + } + + + interface GenericRunner { + + O run(); + } + + static class FutureRunner implements GenericRunner> { + @Override + public Future run() { + return CompletableFuture.runAsync(() -> { + }); + } + } +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java index 347581ceba0f..616c855f3e32 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.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. @@ -34,7 +34,7 @@ * @author Chris Beams * @since 06.04.2004 */ -public class ConcurrencyThrottleInterceptorTests { +class ConcurrencyThrottleInterceptorTests { protected static final Log logger = LogFactory.getLog(ConcurrencyThrottleInterceptorTests.class); @@ -44,7 +44,7 @@ public class ConcurrencyThrottleInterceptorTests { @Test - public void testSerializable() throws Exception { + void testSerializable() throws Exception { DerivedTestBean tb = new DerivedTestBean(); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setInterfaces(ITestBean.class); @@ -63,12 +63,12 @@ public void testSerializable() throws Exception { } @Test - public void testMultipleThreadsWithLimit1() { + void testMultipleThreadsWithLimit1() { testMultipleThreads(1); } @Test - public void testMultipleThreadsWithLimit10() { + void testMultipleThreadsWithLimit10() { testMultipleThreads(10); } @@ -111,8 +111,8 @@ private void testMultipleThreads(int concurrencyLimit) { private static class ConcurrencyThread extends Thread { - private ITestBean proxy; - private Throwable ex; + private final ITestBean proxy; + private final Throwable ex; public ConcurrencyThread(ITestBean proxy, Throwable ex) { this.proxy = proxy; diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java index 81446953f799..44523196bbbf 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.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 @@ void exceptionPathLogsCorrectly() throws Throwable { void sunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() throws Throwable { MethodInvocation methodInvocation = mock(); - given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString", new Class[0])); + given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString")); given(methodInvocation.getThis()).willReturn(this); given(methodInvocation.getArguments()).willReturn(new Object[]{"$ One \\$", 2L}); given(methodInvocation.proceed()).willReturn("Hello!"); @@ -177,7 +177,6 @@ void sunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() throws Thr * is properly configured in {@link CustomizableTraceInterceptor}. */ @Test - @SuppressWarnings("deprecation") void supportedPlaceholderValues() { assertThat(ALLOWED_PLACEHOLDERS).containsExactlyInAnyOrderElementsOf(getPlaceholderConstantValues()); } diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java index abdfd1200372..0c462a7c86a2 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.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. @@ -30,15 +30,15 @@ import static org.mockito.Mockito.verify; /** - * Unit tests for the {@link DebugInterceptor} class. + * Tests for {@link DebugInterceptor}. * * @author Rick Evans * @author Chris Beams */ -public class DebugInterceptorTests { +class DebugInterceptorTests { @Test - public void testSunnyDayPathLogsCorrectly() throws Throwable { + void testSunnyDayPathLogsCorrectly() throws Throwable { MethodInvocation methodInvocation = mock(); Log log = mock(); @@ -52,7 +52,7 @@ public void testSunnyDayPathLogsCorrectly() throws Throwable { } @Test - public void testExceptionPathStillLogsCorrectly() throws Throwable { + void testExceptionPathStillLogsCorrectly() throws Throwable { MethodInvocation methodInvocation = mock(); IllegalArgumentException exception = new IllegalArgumentException(); diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java index 9bd43d1aef3f..565e3e005a42 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,9 +29,9 @@ * @author Rod Johnson * @author Chris Beams */ -public class ExposeBeanNameAdvisorsTests { +class ExposeBeanNameAdvisorsTests { - private class RequiresBeanNameBoundTestBean extends TestBean { + private static class RequiresBeanNameBoundTestBean extends TestBean { private final String beanName; public RequiresBeanNameBoundTestBean(String beanName) { @@ -46,7 +46,7 @@ public int getAge() { } @Test - public void testNoIntroduction() { + void testNoIntroduction() { String beanName = "foo"; TestBean target = new RequiresBeanNameBoundTestBean(beanName); ProxyFactory pf = new ProxyFactory(target); @@ -61,7 +61,7 @@ public void testNoIntroduction() { } @Test - public void testWithIntroduction() { + void testWithIntroduction() { String beanName = "foo"; TestBean target = new RequiresBeanNameBoundTestBean(beanName); ProxyFactory pf = new ProxyFactory(target); diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java index 331dc2b86809..79726a94b4d0 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.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. @@ -31,10 +31,10 @@ * @author Rod Johnson * @author Chris Beams */ -public class ExposeInvocationInterceptorTests { +class ExposeInvocationInterceptorTests { @Test - public void testXmlConfig() { + void testXmlConfig() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(ExposeInvocationInterceptorTests.class, "context.xml")); diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java index 76cada1837f9..6cc67b4da573 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.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. @@ -32,10 +32,10 @@ * @author Rick Evans * @author Chris Beams */ -public class PerformanceMonitorInterceptorTests { +class PerformanceMonitorInterceptorTests { @Test - public void testSuffixAndPrefixAssignment() { + void testSuffixAndPrefixAssignment() { PerformanceMonitorInterceptor interceptor = new PerformanceMonitorInterceptor(); assertThat(interceptor.getPrefix()).isNotNull(); @@ -49,9 +49,9 @@ public void testSuffixAndPrefixAssignment() { } @Test - public void testSunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { + void testSunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { MethodInvocation mi = mock(); - given(mi.getMethod()).willReturn(String.class.getMethod("toString", new Class[0])); + given(mi.getMethod()).willReturn(String.class.getMethod("toString")); Log log = mock(); @@ -62,10 +62,10 @@ public void testSunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { } @Test - public void testExceptionPathStillLogsPerformanceMetricsCorrectly() throws Throwable { + void testExceptionPathStillLogsPerformanceMetricsCorrectly() throws Throwable { MethodInvocation mi = mock(); - given(mi.getMethod()).willReturn(String.class.getMethod("toString", new Class[0])); + given(mi.getMethod()).willReturn(String.class.getMethod("toString")); given(mi.proceed()).willThrow(new IllegalArgumentException()); Log log = mock(); diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java index ee96500febe1..b977f97a4c00 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.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. @@ -29,15 +29,15 @@ import static org.mockito.Mockito.verify; /** - * Unit tests for the {@link SimpleTraceInterceptor} class. + * Tests for {@link SimpleTraceInterceptor}. * * @author Rick Evans * @author Chris Beams */ -public class SimpleTraceInterceptorTests { +class SimpleTraceInterceptorTests { @Test - public void testSunnyDayPathLogsCorrectly() throws Throwable { + void testSunnyDayPathLogsCorrectly() throws Throwable { MethodInvocation mi = mock(); given(mi.getMethod()).willReturn(String.class.getMethod("toString")); given(mi.getThis()).willReturn(this); @@ -51,7 +51,7 @@ public void testSunnyDayPathLogsCorrectly() throws Throwable { } @Test - public void testExceptionPathStillLogsCorrectly() throws Throwable { + void testExceptionPathStillLogsCorrectly() throws Throwable { MethodInvocation mi = mock(); given(mi.getMethod()).willReturn(String.class.getMethod("toString")); given(mi.getThis()).willReturn(this); diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java index 8affdf3bd3d6..ee21418518b5 100644 --- a/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.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. @@ -24,36 +24,36 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for the {@link DefaultScopedObject} class. + * Tests for {@link DefaultScopedObject}. * * @author Rick Evans * @author Chris Beams */ -public class DefaultScopedObjectTests { +class DefaultScopedObjectTests { private static final String GOOD_BEAN_NAME = "foo"; @Test - public void testCtorWithNullBeanFactory() throws Exception { + void testCtorWithNullBeanFactory() { assertThatIllegalArgumentException().isThrownBy(() -> new DefaultScopedObject(null, GOOD_BEAN_NAME)); } @Test - public void testCtorWithNullTargetBeanName() throws Exception { + void testCtorWithNullTargetBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> testBadTargetBeanName(null)); } @Test - public void testCtorWithEmptyTargetBeanName() throws Exception { + void testCtorWithEmptyTargetBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> testBadTargetBeanName("")); } @Test - public void testCtorWithJustWhitespacedTargetBeanName() throws Exception { + void testCtorWithJustWhitespacedTargetBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> testBadTargetBeanName(" ")); } diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java index a746532495b0..0a8b727a9435 100644 --- a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -31,16 +31,16 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class ScopedProxyAutowireTests { +class ScopedProxyAutowireTests { @Test - public void testScopedProxyInheritsAutowireCandidateFalse() { + void testScopedProxyInheritsAutowireCandidateFalse() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(ScopedProxyAutowireTests.class, "scopedAutowireFalse.xml")); - assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false)).contains("scoped")).isTrue(); - assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false)).contains("scoped")).isTrue(); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false))).contains("scoped"); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false))).contains("scoped"); assertThat(bf.containsSingleton("scoped")).isFalse(); TestBean autowired = (TestBean) bf.getBean("autowired"); TestBean unscoped = (TestBean) bf.getBean("unscoped"); @@ -48,13 +48,13 @@ public void testScopedProxyInheritsAutowireCandidateFalse() { } @Test - public void testScopedProxyReplacesAutowireCandidateTrue() { + void testScopedProxyReplacesAutowireCandidateTrue() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(ScopedProxyAutowireTests.class, "scopedAutowireTrue.xml")); - assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false)).contains("scoped")).isTrue(); - assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false)).contains("scoped")).isTrue(); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false))).contains("scoped"); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false))).contains("scoped"); assertThat(bf.containsSingleton("scoped")).isFalse(); TestBean autowired = (TestBean) bf.getBean("autowired"); TestBean scoped = (TestBean) bf.getBean("scoped"); diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java index b69616f49757..ff28009a7fb6 100644 --- a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for {@link ScopedProxyUtils}. + * Tests for {@link ScopedProxyUtils}. * * @author Sam Brannen * @since 5.1.10 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 160be92a2a5b..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 @@ -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. @@ -17,18 +17,21 @@ package org.springframework.aop.support; 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; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.aop.target.EmptyTargetSource; import org.springframework.aop.testfixture.interceptor.NopInterceptor; 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; @@ -37,11 +40,12 @@ * @author Rod Johnson * @author Chris Beams * @author Sebastien Deleuze + * @author Juergen Hoeller */ -public class AopUtilsTests { +class AopUtilsTests { @Test - public void testPointcutCanNeverApply() { + void testPointcutCanNeverApply() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazzy) { @@ -54,13 +58,13 @@ public boolean matches(Method method, @Nullable Class clazzy) { } @Test - public void testPointcutAlwaysApplies() { + void testPointcutAlwaysApplies() { assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), Object.class)).isTrue(); assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), TestBean.class)).isTrue(); } @Test - public void testPointcutAppliesToOneMethodOnObject() { + void testPointcutAppliesToOneMethodOnObject() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazz) { @@ -80,7 +84,7 @@ public boolean matches(Method method, @Nullable Class clazz) { * that's subverted the singleton construction limitation. */ @Test - public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { + void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { assertThat(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)).isSameAs(MethodMatcher.TRUE); assertThat(SerializationTestUtils.serializeAndDeserialize(ClassFilter.TRUE)).isSameAs(ClassFilter.TRUE); assertThat(SerializationTestUtils.serializeAndDeserialize(Pointcut.TRUE)).isSameAs(Pointcut.TRUE); @@ -91,7 +95,7 @@ public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throw } @Test - public void testInvokeJoinpointUsingReflection() throws Throwable { + void testInvokeJoinpointUsingReflection() throws Throwable { String name = "foo"; TestBean testBean = new TestBean(name); Method method = ReflectionUtils.findMethod(TestBean.class, "getName"); @@ -99,4 +103,36 @@ public void testInvokeJoinpointUsingReflection() throws Throwable { assertThat(result).isEqualTo(name); } + @Test // gh-32365 + void mostSpecificMethodBetweenJdkProxyAndTarget() throws Exception { + Class proxyClass = new ProxyFactory(new WithInterface()).getProxyClass(getClass().getClassLoader()); + Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithInterface.class); + assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class); + } + + @Test // gh-32365 + void mostSpecificMethodBetweenCglibProxyAndTarget() throws Exception { + Class proxyClass = new ProxyFactory(new WithoutInterface()).getProxyClass(getClass().getClassLoader()); + Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithoutInterface.class); + assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class); + } + + + interface ProxyInterface { + + void handle(List list); + } + + static class WithInterface implements ProxyInterface { + + public void handle(List list) { + } + } + + static class WithoutInterface { + + public void handle(List list) { + } + } + } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java index b108eab36acb..f85b93bfcb82 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.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. @@ -29,7 +29,7 @@ import static org.mockito.Mockito.verify; /** - * Unit tests for {@link ClassFilters}. + * Tests for {@link ClassFilters}. * * @author Rod Johnson * @author Chris Beams diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java index f1fffbdf9cb4..58b41a6500e8 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java @@ -25,6 +25,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * AOP-specific tests for {@link ClassUtils}. + * * @author Colin Sampaleanu * @author Juergen Hoeller * @author Rob Harrop 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 1306d6f67555..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 @@ -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.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; @@ -33,7 +33,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class ComposablePointcutTests { +class ComposablePointcutTests { public static MethodMatcher GETTER_METHOD_MATCHER = new StaticMethodMatcher() { @Override @@ -56,23 +56,16 @@ public boolean matches(Method m, @Nullable Class targetClass) { } }; - public static MethodMatcher SETTER_METHOD_MATCHER = new StaticMethodMatcher() { - @Override - public boolean matches(Method m, @Nullable Class targetClass) { - return m.getName().startsWith("set"); - } - }; - @Test - public void testMatchAll() throws NoSuchMethodException { + void testMatchAll() throws NoSuchMethodException { Pointcut pc = new ComposablePointcut(); assertThat(pc.getClassFilter().matches(Object.class)).isTrue(); assertThat(pc.getMethodMatcher().matches(Object.class.getMethod("hashCode"), Exception.class)).isTrue(); } @Test - public void testFilterByClass() throws NoSuchMethodException { + void testFilterByClass() { ComposablePointcut pc = new ComposablePointcut(); assertThat(pc.getClassFilter().matches(Object.class)).isTrue(); @@ -92,7 +85,7 @@ public void testFilterByClass() throws NoSuchMethodException { } @Test - public void testUnionMethodMatcher() { + void testUnionMethodMatcher() { // Matches the getAge() method in any class ComposablePointcut pc = new ComposablePointcut(ClassFilter.TRUE, GET_AGE_METHOD_MATCHER); assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); @@ -115,7 +108,7 @@ public void testUnionMethodMatcher() { } @Test - public void testIntersectionMethodMatcher() { + void testIntersectionMethodMatcher() { ComposablePointcut pc = new ComposablePointcut(); assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isTrue(); assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); @@ -132,7 +125,7 @@ public void testIntersectionMethodMatcher() { } @Test - public void testEqualsAndHashCode() throws Exception { + void testEqualsAndHashCode() { ComposablePointcut pc1 = new ComposablePointcut(); ComposablePointcut pc2 = new ComposablePointcut(); @@ -141,7 +134,7 @@ public void testEqualsAndHashCode() throws Exception { pc1.intersection(GETTER_METHOD_MATCHER); - assertThat(pc1.equals(pc2)).isFalse(); + assertThat(pc1).isNotEqualTo(pc2); assertThat(pc1.hashCode()).isNotEqualTo(pc2.hashCode()); pc2.intersection(GETTER_METHOD_MATCHER); diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java index 3401b6fa18bd..a291b4a6e6a1 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.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. @@ -93,7 +93,7 @@ void controlFlowPointcutIsExtensible() { /** * Check that we can use a cflow pointcut in conjunction with - * a static pointcut: e.g. all setter methods that are invoked under + * a static pointcut: for example, all setter methods that are invoked under * a particular class. This greatly reduces the number of calls * to the cflow pointcut, meaning that it's not so prohibitively * expensive. @@ -203,7 +203,7 @@ private static void assertMatchesSetAndGetAge(ControlFlowPointcut cflow, int eva // Will not be advised: not under MyComponent assertThat(proxy.getAge()).isEqualTo(target.getAge()); - assertThat(cflow.getEvaluations()).isEqualTo(1 * evaluationFactor); + assertThat(cflow.getEvaluations()).isEqualTo(evaluationFactor); assertThat(nop.getCount()).isEqualTo(0); // Will be advised: the proxy is invoked under MyComponent#getAge diff --git a/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java index 86128dd4ca63..de5a55463999 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.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. @@ -47,14 +47,14 @@ class DelegatingIntroductionInterceptorTests { @Test - void testNullTarget() throws Exception { + void testNullTarget() { // Shouldn't accept null target assertThatIllegalArgumentException().isThrownBy(() -> new DelegatingIntroductionInterceptor(null)); } @Test - void testIntroductionInterceptorWithDelegation() throws Exception { + void testIntroductionInterceptorWithDelegation() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(TimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -70,7 +70,7 @@ void testIntroductionInterceptorWithDelegation() throws Exception { } @Test - void testIntroductionInterceptorWithInterfaceHierarchy() throws Exception { + void testIntroductionInterceptorWithInterfaceHierarchy() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(SubTimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -86,7 +86,7 @@ void testIntroductionInterceptorWithInterfaceHierarchy() throws Exception { } @Test - void testIntroductionInterceptorWithSuperInterface() throws Exception { + void testIntroductionInterceptorWithSuperInterface() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(TimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -107,7 +107,7 @@ void testAutomaticInterfaceRecognitionInDelegate() throws Exception { final long t = 1001L; class Tester implements TimeStamped, ITester { @Override - public void foo() throws Exception { + public void foo() { } @Override public long getTimeStamp() { @@ -138,7 +138,7 @@ void testAutomaticInterfaceRecognitionInSubclass() throws Exception { @SuppressWarnings("serial") class TestII extends DelegatingIntroductionInterceptor implements TimeStamped, ITester { @Override - public void foo() throws Exception { + public void foo() { } @Override public long getTimeStamp() { @@ -177,9 +177,8 @@ public long getTimeStamp() { assertThat(o).isNotInstanceOf(TimeStamped.class); } - @SuppressWarnings("serial") @Test - void testIntroductionInterceptorDoesntReplaceToString() throws Exception { + void testIntroductionInterceptorDoesNotReplaceToString() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(TimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -246,7 +245,7 @@ void testSerializableDelegatingIntroductionInterceptorSerializable() throws Exce // Test when target implements the interface: should get interceptor by preference. @Test - void testIntroductionMasksTargetImplementation() throws Exception { + void testIntroductionMasksTargetImplementation() { final long t = 1001L; @SuppressWarnings("serial") class TestII extends DelegatingIntroductionInterceptor implements TimeStamped { 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 7ab8548e2a2b..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 @@ -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.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; @@ -34,7 +34,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class MethodMatchersTests { +class MethodMatchersTests { private static final Method TEST_METHOD = mock(Method.class); @@ -56,19 +56,19 @@ public MethodMatchersTests() throws Exception { @Test - public void testDefaultMatchesAll() throws Exception { + void testDefaultMatchesAll() { MethodMatcher defaultMm = MethodMatcher.TRUE; assertThat(defaultMm.matches(EXCEPTION_GETMESSAGE, Exception.class)).isTrue(); assertThat(defaultMm.matches(ITESTBEAN_SETAGE, TestBean.class)).isTrue(); } @Test - public void testMethodMatcherTrueSerializable() throws Exception { + void testMethodMatcherTrueSerializable() throws Exception { assertThat(MethodMatcher.TRUE).isSameAs(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)); } @Test - public void testSingle() throws Exception { + void testSingle() { MethodMatcher defaultMm = MethodMatcher.TRUE; assertThat(defaultMm.matches(EXCEPTION_GETMESSAGE, Exception.class)).isTrue(); assertThat(defaultMm.matches(ITESTBEAN_SETAGE, TestBean.class)).isTrue(); @@ -80,7 +80,7 @@ public void testSingle() throws Exception { @Test - public void testDynamicAndStaticMethodMatcherIntersection() throws Exception { + void testDynamicAndStaticMethodMatcherIntersection() { MethodMatcher mm1 = MethodMatcher.TRUE; MethodMatcher mm2 = new TestDynamicMethodMatcherWhichMatches(); MethodMatcher intersection = MethodMatchers.intersection(mm1, mm2); @@ -95,7 +95,7 @@ public void testDynamicAndStaticMethodMatcherIntersection() throws Exception { } @Test - public void testStaticMethodMatcherUnion() throws Exception { + void testStaticMethodMatcherUnion() { MethodMatcher getterMatcher = new StartsWithMatcher("get"); MethodMatcher setterMatcher = new StartsWithMatcher("set"); MethodMatcher union = MethodMatchers.union(getterMatcher, setterMatcher); @@ -107,11 +107,11 @@ public void testStaticMethodMatcherUnion() throws Exception { } @Test - public void testUnionEquals() { + void testUnionEquals() { MethodMatcher first = MethodMatchers.union(MethodMatcher.TRUE, MethodMatcher.TRUE); MethodMatcher second = new ComposablePointcut(MethodMatcher.TRUE).union(new ComposablePointcut(MethodMatcher.TRUE)).getMethodMatcher(); - assertThat(first.equals(second)).isTrue(); - assertThat(second.equals(first)).isTrue(); + assertThat(first).isEqualTo(second); + assertThat(second).isEqualTo(first); } @Test 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 9a6f05e10171..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 @@ -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. @@ -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; @@ -31,7 +31,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class PointcutsTests { +class PointcutsTests { public static Method TEST_BEAN_SET_AGE; public static Method TEST_BEAN_GET_AGE; @@ -120,7 +120,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { @Test - public void testTrue() { + void testTrue() { assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_ABSQUATULATE, TestBean.class)).isTrue(); @@ -130,7 +130,7 @@ public void testTrue() { } @Test - public void testMatches() { + void testMatches() { assertThat(Pointcuts.matches(allClassSetterPointcut, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); assertThat(Pointcuts.matches(allClassSetterPointcut, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); assertThat(Pointcuts.matches(allClassSetterPointcut, TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); @@ -143,7 +143,7 @@ public void testMatches() { * Should match all setters and getters on any class */ @Test - public void testUnionOfSettersAndGetters() { + void testUnionOfSettersAndGetters() { Pointcut union = Pointcuts.union(allClassGetterPointcut, allClassSetterPointcut); assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); @@ -151,7 +151,7 @@ public void testUnionOfSettersAndGetters() { } @Test - public void testUnionOfSpecificGetters() { + void testUnionOfSpecificGetters() { Pointcut union = Pointcuts.union(allClassGetAgePointcut, allClassGetNamePointcut); assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); @@ -175,7 +175,7 @@ public void testUnionOfSpecificGetters() { * Second one matches all getters in the MyTestBean class. TestBean getters shouldn't pass. */ @Test - public void testUnionOfAllSettersAndSubclassSetters() { + void testUnionOfAllSettersAndSubclassSetters() { assertThat(Pointcuts.matches(myTestBeanSetterPointcut, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); assertThat(Pointcuts.matches(myTestBeanSetterPointcut, TEST_BEAN_SET_AGE, MyTestBean.class, 6)).isTrue(); assertThat(Pointcuts.matches(myTestBeanSetterPointcut, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); @@ -193,7 +193,7 @@ public void testUnionOfAllSettersAndSubclassSetters() { * it's the union of allClassGetAge and subclass getters */ @Test - public void testIntersectionOfSpecificGettersAndSubclassGetters() { + void testIntersectionOfSpecificGettersAndSubclassGetters() { assertThat(Pointcuts.matches(allClassGetAgePointcut, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); assertThat(Pointcuts.matches(allClassGetAgePointcut, TEST_BEAN_GET_AGE, MyTestBean.class)).isTrue(); assertThat(Pointcuts.matches(myTestBeanGetterPointcut, TEST_BEAN_GET_NAME, TestBean.class)).isFalse(); @@ -239,7 +239,7 @@ public void testIntersectionOfSpecificGettersAndSubclassGetters() { * The intersection of these two pointcuts leaves nothing. */ @Test - public void testSimpleIntersection() { + void testSimpleIntersection() { Pointcut intersection = Pointcuts.intersection(allClassGetterPointcut, allClassSetterPointcut); assertThat(Pointcuts.matches(intersection, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); diff --git a/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java b/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java index c3b546c36049..df0213da7423 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.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. @@ -36,14 +36,14 @@ * @author Rod Johnson * @author Chris Beams */ -public class RegexpMethodPointcutAdvisorIntegrationTests { +class RegexpMethodPointcutAdvisorIntegrationTests { private static final Resource CONTEXT = qualifiedResource(RegexpMethodPointcutAdvisorIntegrationTests.class, "context.xml"); @Test - public void testSinglePattern() throws Throwable { + void testSinglePattern() throws Throwable { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); ITestBean advised = (ITestBean) bf.getBean("settersAdvised"); @@ -62,7 +62,7 @@ public void testSinglePattern() throws Throwable { } @Test - public void testMultiplePatterns() throws Throwable { + void testMultiplePatterns() throws Throwable { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); // This is a CGLIB proxy, so we can proxy it to the target class @@ -86,7 +86,7 @@ public void testMultiplePatterns() throws Throwable { } @Test - public void testSerialization() throws Throwable { + void testSerialization() throws Throwable { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); // This is a CGLIB proxy, so we can proxy it to the target class diff --git a/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java b/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java index e09344b35bc8..ad60e60ac646 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link RootClassFilter}. + * Tests for {@link RootClassFilter}. * * @author Sam Brannen * @since 5.1.10 diff --git a/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java index 4ccbc02290b8..1598de413721 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link AnnotationMatchingPointcut}. + * Tests for {@link AnnotationMatchingPointcut}. * * @author Sam Brannen * @since 5.1.10 diff --git a/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java b/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java index 6637815d058e..11e8adb2da4f 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,13 +30,13 @@ /** * @author Stephane Nicoll */ -public class CommonsPool2TargetSourceProxyTests { +class CommonsPool2TargetSourceProxyTests { private static final Resource CONTEXT = qualifiedResource(CommonsPool2TargetSourceProxyTests.class, "context.xml"); @Test - public void testProxy() throws Exception { + void testProxy() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(CONTEXT); diff --git a/spring-aop/src/test/java/org/springframework/aop/target/HotSwappableTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/HotSwappableTargetSourceTests.java index a396adafdcd4..9676b676a094 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/HotSwappableTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/HotSwappableTargetSourceTests.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. @@ -39,7 +39,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class HotSwappableTargetSourceTests { +class HotSwappableTargetSourceTests { /** Initial count value set in bean factory XML */ private static final int INITIAL_COUNT = 10; @@ -68,7 +68,7 @@ public void close() { * Check it works like a normal invoker */ @Test - public void testBasicFunctionality() { + void testBasicFunctionality() { SideEffectBean proxied = (SideEffectBean) beanFactory.getBean("swappable"); assertThat(proxied.getCount()).isEqualTo(INITIAL_COUNT); proxied.doWork(); @@ -80,7 +80,7 @@ public void testBasicFunctionality() { } @Test - public void testValidSwaps() { + void testValidSwaps() { SideEffectBean target1 = (SideEffectBean) beanFactory.getBean("target1"); SideEffectBean target2 = (SideEffectBean) beanFactory.getBean("target2"); @@ -107,7 +107,7 @@ public void testValidSwaps() { } @Test - public void testRejectsSwapToNull() { + void testRejectsSwapToNull() { HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper"); assertThatIllegalArgumentException().as("Shouldn't be able to swap to invalid value").isThrownBy(() -> swapper.swap(null)) @@ -117,7 +117,7 @@ public void testRejectsSwapToNull() { } @Test - public void testSerialization() throws Exception { + void testSerialization() throws Exception { SerializablePerson sp1 = new SerializablePerson(); sp1.setName("Tony"); SerializablePerson sp2 = new SerializablePerson(); diff --git a/spring-aop/src/test/java/org/springframework/aop/target/LazyCreationTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/LazyCreationTargetSourceTests.java index 4551245335ef..266cfedf5093 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/LazyCreationTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/LazyCreationTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,10 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class LazyCreationTargetSourceTests { +class LazyCreationTargetSourceTests { @Test - public void testCreateLazy() { + void testCreateLazy() { TargetSource targetSource = new AbstractLazyCreationTargetSource() { @Override protected Object createObject() { diff --git a/spring-aop/src/test/java/org/springframework/aop/target/LazyInitTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/LazyInitTargetSourceTests.java index 6c38a0f04fc2..84be7d1a5323 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/LazyInitTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/LazyInitTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -34,7 +34,7 @@ * @author Chris Beams * @since 07.01.2005 */ -public class LazyInitTargetSourceTests { +class LazyInitTargetSourceTests { private static final Class CLASS = LazyInitTargetSourceTests.class; @@ -42,9 +42,11 @@ public class LazyInitTargetSourceTests { private static final Resource CUSTOM_TARGET_CONTEXT = qualifiedResource(CLASS, "customTarget.xml"); private static final Resource FACTORY_BEAN_CONTEXT = qualifiedResource(CLASS, "factoryBean.xml"); + private final DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + @Test - public void testLazyInitSingletonTargetSource() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + void lazyInitSingletonTargetSource() { new XmlBeanDefinitionReader(bf).loadBeanDefinitions(SINGLETON_CONTEXT); bf.preInstantiateSingletons(); @@ -55,8 +57,7 @@ public void testLazyInitSingletonTargetSource() { } @Test - public void testCustomLazyInitSingletonTargetSource() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + void customLazyInitSingletonTargetSource() { new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CUSTOM_TARGET_CONTEXT); bf.preInstantiateSingletons(); @@ -67,25 +68,25 @@ public void testCustomLazyInitSingletonTargetSource() { } @Test - public void testLazyInitFactoryBeanTargetSource() { - DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + @SuppressWarnings("unchecked") + void lazyInitFactoryBeanTargetSource() { new XmlBeanDefinitionReader(bf).loadBeanDefinitions(FACTORY_BEAN_CONTEXT); bf.preInstantiateSingletons(); - Set set1 = (Set) bf.getBean("proxy1"); + Set set1 = (Set) bf.getBean("proxy1"); assertThat(bf.containsSingleton("target1")).isFalse(); - assertThat(set1.contains("10")).isTrue(); + assertThat(set1).contains("10"); assertThat(bf.containsSingleton("target1")).isTrue(); - Set set2 = (Set) bf.getBean("proxy2"); + Set set2 = (Set) bf.getBean("proxy2"); assertThat(bf.containsSingleton("target2")).isFalse(); - assertThat(set2.contains("20")).isTrue(); + assertThat(set2).contains("20"); assertThat(bf.containsSingleton("target2")).isTrue(); } @SuppressWarnings("serial") - public static class CustomLazyInitTargetSource extends LazyInitTargetSource { + static class CustomLazyInitTargetSource extends LazyInitTargetSource { @Override protected void postProcessTargetObject(Object targetObject) { diff --git a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java index 6e857ffe6b18..6846f70962ce 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.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. @@ -36,10 +36,10 @@ * @author Rod Johnson * @author Chris Beams */ -public class PrototypeBasedTargetSourceTests { +class PrototypeBasedTargetSourceTests { @Test - public void testSerializability() throws Exception { + void testSerializability() throws Exception { MutablePropertyValues tsPvs = new MutablePropertyValues(); tsPvs.add("targetBeanName", "person"); RootBeanDefinition tsBd = new RootBeanDefinition(TestTargetSource.class); @@ -56,10 +56,8 @@ public void testSerializability() throws Exception { TestTargetSource cpts = (TestTargetSource) bf.getBean("ts"); TargetSource serialized = SerializationTestUtils.serializeAndDeserialize(cpts); - boolean condition = serialized instanceof SingletonTargetSource; - assertThat(condition).as("Changed to SingletonTargetSource on deserialization").isTrue(); - SingletonTargetSource sts = (SingletonTargetSource) serialized; - assertThat(sts.getTarget()).isNotNull(); + assertThat(serialized).isInstanceOfSatisfying(SingletonTargetSource.class, + sts -> assertThat(sts.getTarget()).isNotNull()); } @@ -72,17 +70,12 @@ private static class TestTargetSource extends AbstractPrototypeBasedTargetSource * state can't prevent serialization from working */ @SuppressWarnings({"unused", "serial"}) - private TestBean thisFieldIsNotSerializable = new TestBean(); + private final TestBean thisFieldIsNotSerializable = new TestBean(); @Override - public Object getTarget() throws Exception { + public Object getTarget() { return newPrototypeInstance(); } - - @Override - public void releaseTarget(Object target) throws Exception { - // Do nothing - } } } diff --git a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeTargetSourceTests.java index 7871e1072d18..ba12b5f6beef 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,7 +30,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class PrototypeTargetSourceTests { +class PrototypeTargetSourceTests { /** Initial count value set in bean factory XML */ private static final int INITIAL_COUNT = 10; @@ -52,7 +52,7 @@ public void setup() { * With the singleton, there will be change. */ @Test - public void testPrototypeAndSingletonBehaveDifferently() { + void testPrototypeAndSingletonBehaveDifferently() { SideEffectBean singleton = (SideEffectBean) beanFactory.getBean("singleton"); assertThat(singleton.getCount()).isEqualTo(INITIAL_COUNT); singleton.doWork(); diff --git a/spring-aop/src/test/java/org/springframework/aop/target/ThreadLocalTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/ThreadLocalTargetSourceTests.java index 7cb788ed1ac8..0c227ecd4be4 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/ThreadLocalTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/ThreadLocalTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -31,7 +31,7 @@ * @author Rod Johnson * @author Chris Beams */ -public class ThreadLocalTargetSourceTests { +class ThreadLocalTargetSourceTests { /** Initial count value set in bean factory XML */ private static final int INITIAL_COUNT = 10; @@ -60,7 +60,7 @@ protected void close() { * with one another. */ @Test - public void testUseDifferentManagedInstancesInSameThread() { + void testUseDifferentManagedInstancesInSameThread() { SideEffectBean apartment = (SideEffectBean) beanFactory.getBean("apartment"); assertThat(apartment.getCount()).isEqualTo(INITIAL_COUNT); apartment.doWork(); @@ -72,7 +72,7 @@ public void testUseDifferentManagedInstancesInSameThread() { } @Test - public void testReuseInSameThread() { + void testReuseInSameThread() { SideEffectBean apartment = (SideEffectBean) beanFactory.getBean("apartment"); assertThat(apartment.getCount()).isEqualTo(INITIAL_COUNT); apartment.doWork(); @@ -86,7 +86,7 @@ public void testReuseInSameThread() { * Relies on introduction. */ @Test - public void testCanGetStatsViaMixin() { + void testCanGetStatsViaMixin() { ThreadLocalTargetSourceStats stats = (ThreadLocalTargetSourceStats) beanFactory.getBean("apartment"); // +1 because creating target for stats call counts assertThat(stats.getInvocationCount()).isEqualTo(1); @@ -104,7 +104,7 @@ public void testCanGetStatsViaMixin() { } @Test - public void testNewThreadHasOwnInstance() throws InterruptedException { + void testNewThreadHasOwnInstance() throws InterruptedException { SideEffectBean apartment = (SideEffectBean) beanFactory.getBean("apartment"); assertThat(apartment.getCount()).isEqualTo(INITIAL_COUNT); apartment.doWork(); @@ -144,7 +144,7 @@ public void run() { * Test for SPR-1442. Destroyed target should re-associated with thread and not throw NPE. */ @Test - public void testReuseDestroyedTarget() { + void testReuseDestroyedTarget() { ThreadLocalTargetSource source = (ThreadLocalTargetSource)this.beanFactory.getBean("threadLocalTs"); // try first time diff --git a/spring-aop/src/test/java/org/springframework/aop/target/dynamic/RefreshableTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/dynamic/RefreshableTargetSourceTests.java index 593b2b27c021..3c81244348d7 100644 --- a/spring-aop/src/test/java/org/springframework/aop/target/dynamic/RefreshableTargetSourceTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/target/dynamic/RefreshableTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,13 +27,13 @@ * @author Rob Harrop * @author Chris Beams */ -public class RefreshableTargetSourceTests { +class RefreshableTargetSourceTests { /** * Test what happens when checking for refresh but not refreshing object. */ @Test - public void testRefreshCheckWithNonRefresh() throws Exception { + void testRefreshCheckWithNonRefresh() throws Exception { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(); ts.setRefreshCheckDelay(0); @@ -49,7 +49,7 @@ public void testRefreshCheckWithNonRefresh() throws Exception { * Test what happens when checking for refresh and refresh occurs. */ @Test - public void testRefreshCheckWithRefresh() throws Exception { + void testRefreshCheckWithRefresh() throws Exception { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(true); ts.setRefreshCheckDelay(0); @@ -65,7 +65,7 @@ public void testRefreshCheckWithRefresh() throws Exception { * Test what happens when no refresh occurs. */ @Test - public void testWithNoRefreshCheck() throws Exception { + void testWithNoRefreshCheck() { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(true); ts.setRefreshCheckDelay(-1); diff --git a/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt new file mode 100644 index 000000000000..51dbfbbb8345 --- /dev/null +++ b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt @@ -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.aop.framework + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +/** + * Tests for Kotlin support in [CglibAopProxy]. + * + * @author Sebastien Deleuze + */ +class CglibAopProxyKotlinTests { + + @Test + fun proxiedInvocation() { + val proxyFactory = ProxyFactory(MyKotlinBean()) + val proxy = proxyFactory.proxy as MyKotlinBean + assertThat(proxy.capitalize("foo")).isEqualTo("FOO") + } + + @Test + fun proxiedUncheckedException() { + val proxyFactory = ProxyFactory(MyKotlinBean()) + val proxy = proxyFactory.proxy as MyKotlinBean + assertThatThrownBy { proxy.uncheckedException() }.isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun proxiedCheckedException() { + val proxyFactory = ProxyFactory(MyKotlinBean()) + val proxy = proxyFactory.proxy as MyKotlinBean + assertThatThrownBy { proxy.checkedException() }.isInstanceOf(CheckedException::class.java) + } + + + open class MyKotlinBean { + + open fun capitalize(value: String) = value.uppercase() + + open fun uncheckedException() { + throw IllegalStateException() + } + + open fun checkedException() { + throw CheckedException() + } + } + + class CheckedException() : Exception() +} diff --git a/spring-aop/src/test/kotlin/org/springframework/aop/framework/CoroutinesUtilsTests.kt b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CoroutinesUtilsTests.kt new file mode 100644 index 000000000000..6e079a70276e --- /dev/null +++ b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CoroutinesUtilsTests.kt @@ -0,0 +1,75 @@ +/* + * 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.aop.framework + +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import kotlin.coroutines.Continuation + +/** + * Tests for [CoroutinesUtils]. + * + * @author Sebastien Deleuze + */ +class CoroutinesUtilsTests { + + @Test + fun awaitSingleNonNullValue() { + val value = "foo" + val continuation = Continuation(CoroutineName("test")) { } + runBlocking { + assertThat(CoroutinesUtils.awaitSingleOrNull(value, continuation)).isEqualTo(value) + } + } + + @Test + fun awaitSingleNullValue() { + val value = null + val continuation = Continuation(CoroutineName("test")) { } + runBlocking { + assertThat(CoroutinesUtils.awaitSingleOrNull(value, continuation)).isNull() + } + } + + @Test + fun awaitSingleMonoValue() { + val value = "foo" + val continuation = Continuation(CoroutineName("test")) { } + runBlocking { + assertThat(CoroutinesUtils.awaitSingleOrNull(Mono.just(value), continuation)).isEqualTo(value) + } + } + + @Test + @Suppress("UNCHECKED_CAST") + fun flow() { + val value1 = "foo" + val value2 = "bar" + val values = Flux.just(value1, value2) + val flow = CoroutinesUtils.asFlow(values) as Flow + runBlocking { + assertThat(flow.toList()).containsExactly(value1, value2) + } + } + +} diff --git a/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt b/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt index 37b302cd529c..f2051dbcb8cc 100644 --- a/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt +++ b/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.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. @@ -31,22 +31,51 @@ import kotlin.coroutines.Continuation */ class AopUtilsKotlinTests { - @Test - fun `Invoking suspending function should return Mono`() { - val value = "foo" - val method = ReflectionUtils.findMethod(AopUtilsKotlinTests::class.java, "suspendingFunction", - String::class.java, Continuation::class.java)!! - val continuation = Continuation(CoroutineName("test")) { } - val result = AopUtils.invokeJoinpointUsingReflection(this, method, arrayOf(value, continuation)) - assertThat(result).isInstanceOfSatisfying(Mono::class.java) { - assertThat(it.block()).isEqualTo(value) - } - } - - @Suppress("unused") - suspend fun suspendingFunction(value: String): String { - delay(1) - return value; - } + @Test + fun `Invoking suspending function should return Mono`() { + val value = "foo" + val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle", + String::class. java, Continuation::class.java)!! + val continuation = Continuation(CoroutineName("test")) { } + val result = AopUtils.invokeJoinpointUsingReflection(WithoutInterface(), method, arrayOf(value, continuation)) + assertThat(result).isInstanceOfSatisfying(Mono::class.java) { + assertThat(it.block()).isEqualTo(value) + } + } + + @Test + fun `Invoking suspending function on bridged method should return Mono`() { + val value = "foo" + val bridgedMethod = ReflectionUtils.findMethod(WithInterface::class.java, "handle", Object::class.java, Continuation::class.java)!! + val continuation = Continuation(CoroutineName("test")) { } + val result = AopUtils.invokeJoinpointUsingReflection(WithInterface(), bridgedMethod, arrayOf(value, continuation)) + assertThat(result).isInstanceOfSatisfying(Mono::class.java) { + assertThat(it.block()).isEqualTo(value) + } + } + + @Suppress("unused") + suspend fun suspendingFunction(value: String): String { + delay(1) + return value + } + + class WithoutInterface { + suspend fun handle(value: String): String { + delay(1) + return value + } + } + + interface ProxyInterface { + suspend fun handle(value: T): T + } + + class WithInterface : ProxyInterface { + override suspend fun handle(value: String): String { + delay(1) + return value + } + } } 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/aspectj/CommonExpressions.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonExpressions.java new file mode 100644 index 000000000000..477ceb1173fa --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonExpressions.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.testfixture.aspectj; + +/** + * Common expressions that are used in tests. + * + * @author Stephane Nicoll + */ +public class CommonExpressions { + + /** + * An expression pointcut that matches all methods + */ + public static final String MATCH_ALL_METHODS = "execution(* *(..))"; + +} 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-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixin.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixin.java index 33ffc13f39f9..6c08f24ce2fc 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixin.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixin.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. @@ -44,9 +44,6 @@ public void unlock() { this.locked = false; } - /** - * @see test.mixin.AopProxyTests.Lockable#locked() - */ @Override public boolean locked() { return this.locked; @@ -54,10 +51,8 @@ public boolean locked() { /** * Note that we need to override around advice. - * If the method is a setter and we're locked, prevent execution. - * Otherwise let super.invoke() handle it, and do normal - * Lockable(this) then target behaviour. - * @see org.aopalliance.MethodInterceptor#invoke(org.aopalliance.MethodInvocation) + * If the method is a setter, and we're locked, prevent execution. + * Otherwise, let super.invoke() handle it. */ @Override public Object invoke(MethodInvocation invocation) throws Throwable { 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/AnnotationAsyncExecutionAspect.aj b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj index 46c1b4530aec..14e4c2154677 100644 --- a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,10 @@ import org.springframework.scheduling.annotation.Async; *

This aspect routes methods marked with the {@link Async} annotation as well as methods * in classes marked with the same. Any method expected to be routed asynchronously must * return either {@code void}, {@link Future}, or a subtype of {@link Future} (in particular, - * Spring's {@link org.springframework.util.concurrent.ListenableFuture}). This aspect, - * therefore, will produce a compile-time error for methods that violate this constraint - * on the return type. If, however, a class marked with {@code @Async} contains a method - * that violates this constraint, it produces only a warning. + * {@link java.util.concurrent.CompletableFuture}). This aspect, therefore, will produce a + * compile-time error for methods that violate this constraint on the return type. If, + * however, a class marked with {@code @Async} contains a method that violates this + * constraint, it produces only a warning. * *

This aspect needs to be injected with an implementation of a task-oriented * {@link java.util.concurrent.Executor} to activate it for a specific thread pool, 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/AspectJJtaTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java index 0ed7ffb69eb8..fc51788cd015 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.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. @@ -23,6 +23,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector; import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.TransactionAttributeSource; /** * {@code @Configuration} class that registers the Spring infrastructure beans necessary @@ -35,14 +36,15 @@ * @see EnableTransactionManagement * @see TransactionManagementConfigurationSelector */ -@Configuration +@Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class AspectJJtaTransactionManagementConfiguration extends AspectJTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public JtaAnnotationTransactionAspect jtaTransactionAspect() { + public JtaAnnotationTransactionAspect jtaTransactionAspect(TransactionAttributeSource transactionAttributeSource) { JtaAnnotationTransactionAspect txAspect = JtaAnnotationTransactionAspect.aspectOf(); + txAspect.setTransactionAttributeSource(transactionAttributeSource); if (this.txManager != null) { txAspect.setTransactionManager(this.txManager); } diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java index 2c99c3050744..4e82c4524a7a 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.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. @@ -24,6 +24,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector; import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.TransactionAttributeSource; /** * {@code @Configuration} class that registers the Spring infrastructure beans necessary @@ -37,14 +38,15 @@ * @see TransactionManagementConfigurationSelector * @see AspectJJtaTransactionManagementConfiguration */ -@Configuration +@Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class AspectJTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ASPECT_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public AnnotationTransactionAspect transactionAspect() { + public AnnotationTransactionAspect transactionAspect(TransactionAttributeSource transactionAttributeSource) { AnnotationTransactionAspect txAspect = AnnotationTransactionAspect.aspectOf(); + txAspect.setTransactionAttributeSource(transactionAttributeSource); if (this.txManager != null) { txAspect.setTransactionManager(this.txManager); } 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/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java index 43947fa29c29..a37e14101732 100644 --- a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java +++ b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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,12 +22,12 @@ /** * @author Adrian Colyer + * @author Juergen Hoeller */ -public class AutoProxyWithCodeStyleAspectsTests { +class AutoProxyWithCodeStyleAspectsTests { @Test - @SuppressWarnings("resource") - public void noAutoproxyingOfAjcCompiledAspects() { + void noAutoProxyingOfAjcCompiledAspects() { new ClassPathXmlApplicationContext("org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml"); } diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java index a02bcad6793b..f47eb7ba3c53 100644 --- a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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,12 +20,14 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; -public class SpringConfiguredWithAutoProxyingTests { +/** + * @author Ramnivas Laddad + * @author Juergen Hoeller + */ +class SpringConfiguredWithAutoProxyingTests { @Test - @SuppressWarnings("resource") - public void springConfiguredAndAutoProxyUsedTogether() { - // instantiation is sufficient to trigger failure if this is going to fail... + void springConfiguredAndAutoProxyUsedTogether() { new ClassPathXmlApplicationContext("org/springframework/beans/factory/aspectj/springConfigured.xml"); } diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java index 71e98f68295d..053417e21878 100644 --- a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,10 +25,10 @@ /** * @author Chris Beams */ -public class XmlBeanConfigurerTests { +class XmlBeanConfigurerTests { @Test - public void injection() { + void injection() { try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/beans/factory/aspectj/beanConfigurerTests.xml")) { diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java index b272e70e307b..8cec3f65ea54 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.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. @@ -451,7 +451,7 @@ protected void testMultiCache(CacheableService service) { protected void testMultiEvict(CacheableService service) { Object o1 = new Object(); - Object o2 = o1.toString() + "A"; + Object o2 = o1 + "A"; Object r1 = service.multiCache(o1); @@ -543,7 +543,7 @@ protected void testMultiConditionalCacheAndEvict(CacheableService service) { Object r1 = service.multiConditionalCacheAndEvict(key); Object r3 = service.multiConditionalCacheAndEvict(key); - assertThat(r1.equals(r3)).isFalse(); + assertThat(r1).isNotEqualTo(r3); assertThat(primary.get(key)).isNull(); Object key2 = 3; @@ -556,132 +556,132 @@ protected void testMultiConditionalCacheAndEvict(CacheableService service) { } @Test - public void testCacheable() { + void testCacheable() { testCacheable(this.cs); } @Test - public void testCacheableNull() { + void testCacheableNull() { testCacheableNull(this.cs); } @Test - public void testCacheableSync() { + void testCacheableSync() { testCacheableSync(this.cs); } @Test - public void testCacheableSyncNull() { + void testCacheableSyncNull() { testCacheableSyncNull(this.cs); } @Test - public void testEvict() { + void testEvict() { testEvict(this.cs, true); } @Test - public void testEvictEarly() { + void testEvictEarly() { testEvictEarly(this.cs); } @Test - public void testEvictWithException() { + void testEvictWithException() { testEvictException(this.cs); } @Test - public void testEvictAll() { + void testEvictAll() { testEvictAll(this.cs, true); } @Test - public void testEvictAllEarly() { + void testEvictAllEarly() { testEvictAllEarly(this.cs); } @Test - public void testEvictWithKey() { + void testEvictWithKey() { testEvictWithKey(this.cs); } @Test - public void testEvictWithKeyEarly() { + void testEvictWithKeyEarly() { testEvictWithKeyEarly(this.cs); } @Test - public void testConditionalExpression() { + void testConditionalExpression() { testConditionalExpression(this.cs); } @Test - public void testConditionalExpressionSync() { + void testConditionalExpressionSync() { testConditionalExpressionSync(this.cs); } @Test - public void testUnlessExpression() { + void testUnlessExpression() { testUnlessExpression(this.cs); } @Test - public void testClassCacheUnlessExpression() { + void testClassCacheUnlessExpression() { testUnlessExpression(this.cs); } @Test - public void testKeyExpression() { + void testKeyExpression() { testKeyExpression(this.cs); } @Test - public void testVarArgsKey() { + void testVarArgsKey() { testVarArgsKey(this.cs); } @Test - public void testClassCacheCacheable() { + void testClassCacheCacheable() { testCacheable(this.ccs); } @Test - public void testClassCacheEvict() { + void testClassCacheEvict() { testEvict(this.ccs, true); } @Test - public void testClassEvictEarly() { + void testClassEvictEarly() { testEvictEarly(this.ccs); } @Test - public void testClassEvictAll() { + void testClassEvictAll() { testEvictAll(this.ccs, true); } @Test - public void testClassEvictWithException() { + void testClassEvictWithException() { testEvictException(this.ccs); } @Test - public void testClassCacheEvictWithWKey() { + void testClassCacheEvictWithWKey() { testEvictWithKey(this.ccs); } @Test - public void testClassEvictWithKeyEarly() { + void testClassEvictWithKeyEarly() { testEvictWithKeyEarly(this.ccs); } @Test - public void testNullValue() { + void testNullValue() { testNullValue(this.cs); } @Test - public void testClassNullValue() { + void testClassNullValue() { Object key = new Object(); assertThat(this.ccs.nullValue(key)).isNull(); int nr = this.ccs.nullInvocations().intValue(); @@ -694,27 +694,27 @@ public void testClassNullValue() { } @Test - public void testMethodName() { + void testMethodName() { testMethodName(this.cs, "name"); } @Test - public void testClassMethodName() { + void testClassMethodName() { testMethodName(this.ccs, "nametestCache"); } @Test - public void testRootVars() { + void testRootVars() { testRootVars(this.cs); } @Test - public void testClassRootVars() { + void testClassRootVars() { testRootVars(this.ccs); } @Test - public void testCustomKeyGenerator() { + void testCustomKeyGenerator() { Object param = new Object(); Object r1 = this.cs.customKeyGenerator(param); assertThat(this.cs.customKeyGenerator(param)).isSameAs(r1); @@ -725,14 +725,14 @@ public void testCustomKeyGenerator() { } @Test - public void testUnknownCustomKeyGenerator() { + void testUnknownCustomKeyGenerator() { Object param = new Object(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> this.cs.unknownCustomKeyGenerator(param)); } @Test - public void testCustomCacheManager() { + void testCustomCacheManager() { CacheManager customCm = this.ctx.getBean("customCacheManager", CacheManager.class); Object key = new Object(); Object r1 = this.cs.customCacheManager(key); @@ -743,139 +743,139 @@ public void testCustomCacheManager() { } @Test - public void testUnknownCustomCacheManager() { + void testUnknownCustomCacheManager() { Object param = new Object(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> this.cs.unknownCustomCacheManager(param)); } @Test - public void testNullArg() { + void testNullArg() { testNullArg(this.cs); } @Test - public void testClassNullArg() { + void testClassNullArg() { testNullArg(this.ccs); } @Test - public void testCheckedException() { + void testCheckedException() { testCheckedThrowable(this.cs); } @Test - public void testClassCheckedException() { + void testClassCheckedException() { testCheckedThrowable(this.ccs); } @Test - public void testCheckedExceptionSync() { + void testCheckedExceptionSync() { testCheckedThrowableSync(this.cs); } @Test - public void testClassCheckedExceptionSync() { + void testClassCheckedExceptionSync() { testCheckedThrowableSync(this.ccs); } @Test - public void testUncheckedException() { + void testUncheckedException() { testUncheckedThrowable(this.cs); } @Test - public void testClassUncheckedException() { + void testClassUncheckedException() { testUncheckedThrowable(this.ccs); } @Test - public void testUncheckedExceptionSync() { + void testUncheckedExceptionSync() { testUncheckedThrowableSync(this.cs); } @Test - public void testClassUncheckedExceptionSync() { + void testClassUncheckedExceptionSync() { testUncheckedThrowableSync(this.ccs); } @Test - public void testUpdate() { + void testUpdate() { testCacheUpdate(this.cs); } @Test - public void testClassUpdate() { + void testClassUpdate() { testCacheUpdate(this.ccs); } @Test - public void testConditionalUpdate() { + void testConditionalUpdate() { testConditionalCacheUpdate(this.cs); } @Test - public void testClassConditionalUpdate() { + void testClassConditionalUpdate() { testConditionalCacheUpdate(this.ccs); } @Test - public void testMultiCache() { + void testMultiCache() { testMultiCache(this.cs); } @Test - public void testClassMultiCache() { + void testClassMultiCache() { testMultiCache(this.ccs); } @Test - public void testMultiEvict() { + void testMultiEvict() { testMultiEvict(this.cs); } @Test - public void testClassMultiEvict() { + void testClassMultiEvict() { testMultiEvict(this.ccs); } @Test - public void testMultiPut() { + void testMultiPut() { testMultiPut(this.cs); } @Test - public void testClassMultiPut() { + void testClassMultiPut() { testMultiPut(this.ccs); } @Test - public void testPutRefersToResult() { + void testPutRefersToResult() { testPutRefersToResult(this.cs); } @Test - public void testClassPutRefersToResult() { + void testClassPutRefersToResult() { testPutRefersToResult(this.ccs); } @Test - public void testMultiCacheAndEvict() { + void testMultiCacheAndEvict() { testMultiCacheAndEvict(this.cs); } @Test - public void testClassMultiCacheAndEvict() { + void testClassMultiCacheAndEvict() { testMultiCacheAndEvict(this.ccs); } @Test - public void testMultiConditionalCacheAndEvict() { + void testMultiConditionalCacheAndEvict() { testMultiConditionalCacheAndEvict(this.cs); } @Test - public void testClassMultiConditionalCacheAndEvict() { + void testClassMultiConditionalCacheAndEvict() { testMultiConditionalCacheAndEvict(this.ccs); } diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java index 4601c0ea814c..ffeab37c4213 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ /** * @author Costin Leau */ -public class AspectJCacheAnnotationTests extends AbstractCacheAnnotationTests { +class AspectJCacheAnnotationTests extends AbstractCacheAnnotationTests { @Override protected ConfigurableApplicationContext getApplicationContext() { @@ -37,7 +37,7 @@ protected ConfigurableApplicationContext getApplicationContext() { } @Test - public void testKeyStrategy() { + void testKeyStrategy() { AnnotationCacheAspect aspect = ctx.getBean( "org.springframework.cache.config.internalCacheAspect", AnnotationCacheAspect.class); assertThat(aspect.getKeyGenerator()).isSameAs(ctx.getBean("keyGenerator")); 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 7d73e484b4cf..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 @@ -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,6 +20,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurer; import org.springframework.cache.annotation.EnableCaching; @@ -47,7 +49,7 @@ /** * @author Stephane Nicoll */ -public class AspectJEnableCachingIsolatedTests { +class AspectJEnableCachingIsolatedTests { private ConfigurableApplicationContext ctx; @@ -65,14 +67,14 @@ public void closeContext() { @Test - public void testKeyStrategy() { + void testKeyStrategy() { load(EnableCachingConfig.class); AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); assertThat(aspect.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator", KeyGenerator.class)); } @Test - public void testCacheErrorHandler() { + void testCacheErrorHandler() { load(EnableCachingConfig.class); AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); assertThat(aspect.getErrorHandler()).isSameAs(this.ctx.getBean("errorHandler", CacheErrorHandler.class)); @@ -82,27 +84,31 @@ public void testCacheErrorHandler() { // --- local tests ------- @Test - public void singleCacheManagerBean() { + void singleCacheManagerBean() { load(SingleCacheManagerConfig.class); } @Test - public void multipleCacheManagerBeans() { + void multipleCacheManagerBeans() { try { load(MultiCacheManagerConfig.class); } - catch (IllegalStateException ex) { - assertThat(ex.getMessage()).contains("bean of type CacheManager"); + catch (NoUniqueBeanDefinitionException ex) { + assertThat(ex.getMessage()).contains( + "no CacheResolver specified and expected single matching CacheManager but found 2") + .contains("cm1", "cm2"); + assertThat(ex.getNumberOfBeansFound()).isEqualTo(2); + assertThat(ex.getBeanNamesFound()).containsExactlyInAnyOrder("cm1", "cm2"); } } @Test - public void multipleCacheManagerBeans_implementsCachingConfigurer() { + void multipleCacheManagerBeans_implementsCachingConfigurer() { load(MultiCacheManagerConfigurer.class); // does not throw } @Test - public void multipleCachingConfigurers() { + void multipleCachingConfigurers() { try { load(MultiCacheManagerConfigurer.class, EnableCachingConfig.class); } @@ -112,12 +118,12 @@ public void multipleCachingConfigurers() { } @Test - public void noCacheManagerBeans() { + void noCacheManagerBeans() { try { load(EmptyConfig.class); } - catch (IllegalStateException ex) { - assertThat(ex.getMessage()).contains("no bean of type CacheManager"); + catch (NoSuchBeanDefinitionException ex) { + assertThat(ex.getMessage()).contains("no CacheResolver specified"); } } @@ -132,7 +138,7 @@ public void emptyConfigSupport() { } @Test - public void bothSetOnlyResolverIsUsed() { + void bothSetOnlyResolverIsUsed() { load(FullCachingConfig.class); AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java index 4c5f7b414ab5..8b3b440782ce 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -37,7 +37,7 @@ /** * @author Stephane Nicoll */ -public class AspectJEnableCachingTests extends AbstractCacheAnnotationTests { +class AspectJEnableCachingTests extends AbstractCacheAnnotationTests { @Override protected ConfigurableApplicationContext getApplicationContext() { diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java index dc278365d727..a106633859c1 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 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. @@ -34,7 +34,7 @@ /** * @author Stephane Nicoll */ -public class JCacheAspectJJavaConfigTests extends AbstractJCacheAnnotationTests { +class JCacheAspectJJavaConfigTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java index c755d8c3f4aa..a2879aa57639 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.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. @@ -24,7 +24,7 @@ * @author Stephane Nicoll * @author Sam Brannen */ -public class JCacheAspectJNamespaceConfigTests extends AbstractJCacheAnnotationTests { +class JCacheAspectJNamespaceConfigTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { 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/context/annotation/aspectj/AnnotationBeanConfigurerTests.java b/spring-aspects/src/test/java/org/springframework/context/annotation/aspectj/AnnotationBeanConfigurerTests.java index c914de1a061a..49544d99e051 100644 --- a/spring-aspects/src/test/java/org/springframework/context/annotation/aspectj/AnnotationBeanConfigurerTests.java +++ b/spring-aspects/src/test/java/org/springframework/context/annotation/aspectj/AnnotationBeanConfigurerTests.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,10 +33,10 @@ * @author Chris Beams * @since 3.1 */ -public class AnnotationBeanConfigurerTests { +class AnnotationBeanConfigurerTests { @Test - public void injection() { + void injection() { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) { ShouldBeConfiguredBySpring myObject = new ShouldBeConfiguredBySpring(); assertThat(myObject.getName()).isEqualTo("Rod"); 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 624d96a27709..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,13 +35,12 @@ 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; /** - * Unit tests for {@link AnnotationAsyncExecutionAspect}. + * Tests for {@link AnnotationAsyncExecutionAspect}. * * @author Ramnivas Laddad * @author Stephane Nicoll @@ -64,7 +63,7 @@ public void setUp() { @Test - public void asyncMethodGetsRoutedAsynchronously() { + void asyncMethodGetsRoutedAsynchronously() { ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation(); obj.incrementAsync(); executor.waitForCompletion(); @@ -74,7 +73,7 @@ public void asyncMethodGetsRoutedAsynchronously() { } @Test - public void asyncMethodReturningFutureGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { + void asyncMethodReturningFutureGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation(); Future future = obj.incrementReturningAFuture(); // No need to executor.waitForCompletion() as future.get() will have the same effect @@ -85,7 +84,7 @@ public void asyncMethodReturningFutureGetsRoutedAsynchronouslyAndReturnsAFuture( } @Test - public void syncMethodGetsRoutedSynchronously() { + void syncMethodGetsRoutedSynchronously() { ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation(); obj.increment(); assertThat(obj.counter).isEqualTo(1); @@ -94,7 +93,7 @@ public void syncMethodGetsRoutedSynchronously() { } @Test - public void voidMethodInAsyncClassGetsRoutedAsynchronously() { + void voidMethodInAsyncClassGetsRoutedAsynchronously() { ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation(); obj.increment(); executor.waitForCompletion(); @@ -104,7 +103,7 @@ public void voidMethodInAsyncClassGetsRoutedAsynchronously() { } @Test - public void methodReturningFutureInAsyncClassGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { + void methodReturningFutureInAsyncClassGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation(); Future future = obj.incrementReturningAFuture(); assertThat(future.get().intValue()).isEqualTo(5); @@ -115,7 +114,7 @@ public void methodReturningFutureInAsyncClassGetsRoutedAsynchronouslyAndReturnsA /* @Test - public void methodReturningNonVoidNonFutureInAsyncClassGetsRoutedSynchronously() { + void methodReturningNonVoidNonFutureInAsyncClassGetsRoutedSynchronously() { ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation(); int returnValue = obj.return5(); assertEquals(5, returnValue); @@ -125,7 +124,7 @@ public void methodReturningNonVoidNonFutureInAsyncClassGetsRoutedSynchronously() */ @Test - public void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws InterruptedException, ExecutionException { + void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws InterruptedException, ExecutionException { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition("e1", new RootBeanDefinition(ThreadPoolTaskExecutor.class)); AnnotationAsyncExecutionAspect.aspectOf().setBeanFactory(beanFactory); @@ -136,15 +135,12 @@ public void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws Interrupted 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-"); } @Test - public void exceptionHandlerCalled() { + void exceptionHandlerCalled() { Method m = ReflectionUtils.findMethod(ClassWithException.class, "failWithVoid"); TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler(); AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(exceptionHandler); @@ -161,7 +157,7 @@ public void exceptionHandlerCalled() { } @Test - public void exceptionHandlerNeverThrowsUnexpectedException() { + void exceptionHandlerNeverThrowsUnexpectedException() { Method m = ReflectionUtils.findMethod(ClassWithException.class, "failWithVoid"); TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler(true); AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(exceptionHandler); @@ -221,7 +217,7 @@ public void increment() { @Async public Future incrementReturningAFuture() { counter++; - return new AsyncResult(5); + return new AsyncResult<>(5); } /** @@ -256,7 +252,7 @@ public int return5() { public Future incrementReturningAFuture() { counter++; - return new AsyncResult(5); + return new AsyncResult<>(5); } } @@ -265,16 +261,11 @@ static class ClassWithQualifiedAsyncMethods { @Async public Future defaultWork() { - return new AsyncResult(Thread.currentThread()); - } - - @Async("e1") - public ListenableFuture e1Work() { - return new AsyncResult(Thread.currentThread()); + return new AsyncResult<>(Thread.currentThread()); } @Async("e1") - public CompletableFuture e1OtherWork() { + public CompletableFuture e1Work() { return CompletableFuture.completedFuture(Thread.currentThread()); } } diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java index 20c22f4dbb75..ee8799a68f15 100644 --- a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java +++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -32,7 +32,7 @@ /** * @author Stephane Nicoll */ -public class AnnotationDrivenBeanDefinitionParserTests { +class AnnotationDrivenBeanDefinitionParserTests { private ConfigurableApplicationContext context; @@ -50,7 +50,7 @@ public void after() { } @Test - public void asyncAspectRegistered() { + void asyncAspectRegistered() { assertThat(context.containsBean(TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME)).isTrue(); } diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java index 616e42996080..86d575febd6a 100644 --- a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java +++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -74,14 +74,6 @@ public void await(long timeout) { } } - private static final class UncaughtExceptionDescriptor { - private final Throwable ex; - - private final Method method; - - private UncaughtExceptionDescriptor(Throwable ex, Method method) { - this.ex = ex; - this.method = method; - } + private record UncaughtExceptionDescriptor(Throwable ex, Method method) { } } diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java index b6ef121a7962..0a0280aa8b89 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -47,14 +47,14 @@ public void setUp() { } @Test - public void commitOnAnnotatedPublicMethod() throws Throwable { + void commitOnAnnotatedPublicMethod() throws Throwable { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationPublicAnnotatedMember().echo(null); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void matchingRollbackOnApplied() throws Throwable { + void matchingRollbackOnApplied() { assertThat(this.txManager.begun).isEqualTo(0); InterruptedException test = new InterruptedException(); assertThatExceptionOfType(InterruptedException.class).isThrownBy(() -> @@ -65,7 +65,7 @@ public void matchingRollbackOnApplied() throws Throwable { } @Test - public void nonMatchingRollbackOnApplied() throws Throwable { + void nonMatchingRollbackOnApplied() { assertThat(this.txManager.begun).isEqualTo(0); IOException test = new IOException(); assertThatIOException().isThrownBy(() -> @@ -76,35 +76,35 @@ public void nonMatchingRollbackOnApplied() throws Throwable { } @Test - public void commitOnAnnotatedProtectedMethod() { + void commitOnAnnotatedProtectedMethod() { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationProtectedAnnotatedMember().doInTransaction(); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void nonAnnotatedMethodCallingProtectedMethod() { + void nonAnnotatedMethodCallingProtectedMethod() { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationProtectedAnnotatedMember().doSomething(); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void commitOnAnnotatedPrivateMethod() { + void commitOnAnnotatedPrivateMethod() { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationPrivateAnnotatedMember().doInTransaction(); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void nonAnnotatedMethodCallingPrivateMethod() { + void nonAnnotatedMethodCallingPrivateMethod() { assertThat(this.txManager.begun).isEqualTo(0); new JtaAnnotationPrivateAnnotatedMember().doSomething(); assertThat(this.txManager.commits).isEqualTo(1); } @Test - public void notTransactional() { + void notTransactional() { assertThat(this.txManager.begun).isEqualTo(0); new TransactionAspectTests.NotTransactional().noop(); assertThat(this.txManager.begun).isEqualTo(0); diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java index 8c790bf2dc26..d706674d7204 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.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. @@ -32,7 +32,7 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class TransactionAspectTests { +class TransactionAspectTests { private final CallCountingTransactionManager txManager = new CallCountingTransactionManager(); @@ -56,7 +56,7 @@ public void initContext() { @Test - public void testCommitOnAnnotatedClass() throws Throwable { + void testCommitOnAnnotatedClass() throws Throwable { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); annotationOnlyOnClassWithNoInterface.echo(null); @@ -64,7 +64,7 @@ public void testCommitOnAnnotatedClass() throws Throwable { } @Test - public void commitOnAnnotatedProtectedMethod() throws Throwable { + void commitOnAnnotatedProtectedMethod() { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); beanWithAnnotatedProtectedMethod.doInTransaction(); @@ -72,7 +72,7 @@ public void commitOnAnnotatedProtectedMethod() throws Throwable { } @Test - public void commitOnAnnotatedPrivateMethod() throws Throwable { + void commitOnAnnotatedPrivateMethod() { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); beanWithAnnotatedPrivateMethod.doSomething(); @@ -80,7 +80,7 @@ public void commitOnAnnotatedPrivateMethod() throws Throwable { } @Test - public void commitOnNonAnnotatedNonPublicMethodInTransactionalType() throws Throwable { + void commitOnNonAnnotatedNonPublicMethodInTransactionalType() { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); annotationOnlyOnClassWithNoInterface.nonTransactionalMethod(); @@ -88,7 +88,7 @@ public void commitOnNonAnnotatedNonPublicMethodInTransactionalType() throws Thro } @Test - public void commitOnAnnotatedMethod() throws Throwable { + void commitOnAnnotatedMethod() throws Throwable { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); methodAnnotationOnly.echo(null); @@ -96,7 +96,7 @@ public void commitOnAnnotatedMethod() throws Throwable { } @Test - public void notTransactional() throws Throwable { + void notTransactional() { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); new NotTransactional().noop(); @@ -104,45 +104,45 @@ public void notTransactional() throws Throwable { } @Test - public void defaultCommitOnAnnotatedClass() throws Throwable { + void defaultCommitOnAnnotatedClass() { Exception ex = new Exception(); assertThatException() - .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), false)) - .isSameAs(ex); + .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), false)) + .isSameAs(ex); } @Test - public void defaultRollbackOnAnnotatedClass() throws Throwable { + void defaultRollbackOnAnnotatedClass() { RuntimeException ex = new RuntimeException(); assertThatRuntimeException() - .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), true)) - .isSameAs(ex); + .isThrownBy(() -> testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), true)) + .isSameAs(ex); } @Test - public void defaultCommitOnSubclassOfAnnotatedClass() throws Throwable { + void defaultCommitOnSubclassOfAnnotatedClass() { Exception ex = new Exception(); assertThatException() - .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalAnnotation().echo(ex), false)) - .isSameAs(ex); + .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalAnnotation().echo(ex), false)) + .isSameAs(ex); } @Test - public void defaultCommitOnSubclassOfClassWithTransactionalMethodAnnotated() throws Throwable { + void defaultCommitOnSubclassOfClassWithTransactionalMethodAnnotated() { Exception ex = new Exception(); assertThatException() - .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalMethodAnnotation().echo(ex), false)) - .isSameAs(ex); + .isThrownBy(() -> testRollback(() -> new SubclassOfClassWithTransactionalMethodAnnotation().echo(ex), false)) + .isSameAs(ex); } @Test - public void noCommitOnImplementationOfAnnotatedInterface() throws Throwable { + void noCommitOnImplementationOfAnnotatedInterface() { Exception ex = new Exception(); testNotTransactional(() -> new ImplementsAnnotatedInterface().echo(ex), ex); } @Test - public void noRollbackOnImplementationOfAnnotatedInterface() throws Throwable { + void noRollbackOnImplementationOfAnnotatedInterface() { Exception rollbackProvokingException = new RuntimeException(); testNotTransactional(() -> new ImplementsAnnotatedInterface().echo(rollbackProvokingException), rollbackProvokingException); @@ -164,12 +164,12 @@ protected void testRollback(TransactionOperationCallback toc, boolean rollback) } } - protected void testNotTransactional(TransactionOperationCallback toc, Throwable expected) throws Throwable { + protected void testNotTransactional(TransactionOperationCallback toc, Throwable expected) { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); assertThatExceptionOfType(Throwable.class) - .isThrownBy(toc::performTransactionalOperation) - .isSameAs(expected); + .isThrownBy(toc::performTransactionalOperation) + .isSameAs(expected); assertThat(txManager.begun).isEqualTo(0); } diff --git a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml index 6be707bf51dd..e6c494c4f966 100644 --- a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml +++ b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml @@ -2,16 +2,29 @@ + http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd + http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache-3.1.xsd + http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd"> - + + + + + + + - + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml index 54ddbfd44a7b..9366abc646ee 100644 --- a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml +++ b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml @@ -24,8 +24,7 @@ - + diff --git a/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml index 61d1d3a8e9bf..2bc3dc1d113d 100644 --- a/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml +++ b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml @@ -7,12 +7,10 @@ http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd"> - + - + diff --git a/spring-beans/spring-beans.gradle b/spring-beans/spring-beans.gradle index b407bf0ed249..a725741630b2 100644 --- a/spring-beans/spring-beans.gradle +++ b/spring-beans/spring-beans.gradle @@ -11,7 +11,6 @@ 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"))) diff --git a/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java index ec828fb185b6..58e6b215bba9 100644 --- a/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java +++ b/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -55,30 +55,30 @@ public void setup() { RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); switch (this.mode) { - case "simple": - break; - case "dependencyCheck": + case "simple" -> { + } + case "dependencyCheck" -> { rbd = new RootBeanDefinition(LifecycleBean.class); rbd.setDependencyCheck(RootBeanDefinition.DEPENDENCY_CHECK_OBJECTS); this.beanFactory.addBeanPostProcessor(new LifecycleBean.PostProcessor()); - break; - case "constructor": + } + case "constructor" -> { rbd.getConstructorArgumentValues().addGenericArgumentValue("juergen"); rbd.getConstructorArgumentValues().addGenericArgumentValue("99"); - break; - case "constructorArgument": + } + case "constructorArgument" -> { rbd.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference("spouse")); this.beanFactory.registerBeanDefinition("test", rbd); this.beanFactory.registerBeanDefinition("spouse", new RootBeanDefinition(TestBean.class)); - break; - case "properties": + } + case "properties" -> { rbd.getPropertyValues().add("name", "juergen"); rbd.getPropertyValues().add("age", "99"); - break; - case "resolvedProperties": + } + case "resolvedProperties" -> { rbd.getPropertyValues().add("spouse", new RuntimeBeanReference("spouse")); this.beanFactory.registerBeanDefinition("spouse", new RootBeanDefinition(TestBean.class)); - break; + } } rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); this.beanFactory.registerBeanDefinition("test", rbd); diff --git a/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt b/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt index 9bbdd9711df0..3d06b4245423 100644 --- a/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt +++ b/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.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. @@ -16,7 +16,7 @@ package org.springframework.beans -import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeUnit import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode @@ -30,22 +30,22 @@ import org.openjdk.jmh.annotations.State @OutputTimeUnit(TimeUnit.NANOSECONDS) open class KotlinBeanUtilsBenchmark { - private val noArgConstructor = TestClass1::class.java.getDeclaredConstructor() - private val constructor = TestClass2::class.java.getDeclaredConstructor(Int::class.java, String::class.java) + private val noArgConstructor = TestClass1::class.java.getDeclaredConstructor() + private val constructor = TestClass2::class.java.getDeclaredConstructor(Int::class.java, String::class.java) - @Benchmark - fun emptyConstructor(): Any { + @Benchmark + fun emptyConstructor(): Any { return BeanUtils.instantiateClass(noArgConstructor) - } + } - @Benchmark - fun nonEmptyConstructor(): Any { + @Benchmark + fun nonEmptyConstructor(): Any { return BeanUtils.instantiateClass(constructor, 1, "str") - } + } - class TestClass1() + class TestClass1 - @Suppress("UNUSED_PARAMETER") - class TestClass2(int: Int, string: String) + @Suppress("UNUSED_PARAMETER") + class TestClass2(int: Int, string: String) } 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 fcb430112905..ee3e805ddea7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,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('['); @@ -461,7 +457,9 @@ private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) ph.setValue(valueToApply); } catch (TypeMismatchException ex) { - throw ex; + if (!ph.setValueFallbackIfPossible(pv.getValue())) { + throw ex; + } } catch (InvocationTargetException ex) { PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent( @@ -472,7 +470,7 @@ private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) else { Throwable cause = ex.getTargetException(); if (cause instanceof UndeclaredThrowableException) { - // May happen e.g. with Groovy-generated methods + // May happen, for example, with Groovy-generated methods cause = cause.getCause(); } throw new MethodInvocationException(propertyChangeEvent, cause); @@ -486,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) { @@ -514,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); @@ -578,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 { @@ -599,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 { @@ -608,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); @@ -656,6 +648,14 @@ else if (value instanceof List list) { growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1); value = list.get(index); } + else if (value instanceof Map map) { + Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); + // IMPORTANT: Do not pass full property name in here - property editors + // must not kick in for map keys but rather only for map values. + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); + Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); + value = map.get(convertedMapKey); + } else if (value instanceof Iterable iterable) { // Apply index to Iterator in case of a Set/Collection/Iterable. int index = Integer.parseInt(key); @@ -683,14 +683,6 @@ else if (value instanceof Iterable iterable) { currIndex + ", accessed using property path '" + propertyName + "'"); } } - else if (value instanceof Map map) { - Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); - // IMPORTANT: Do not pass full property name in here - property editors - // must not kick in for map keys but rather only for map values. - TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); - Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); - value = map.get(convertedMapKey); - } else { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Property referenced in indexed property path '" + propertyName + @@ -732,8 +724,7 @@ else if (value instanceof Map map) { * 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)); @@ -745,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. @@ -843,8 +833,10 @@ protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(St * @return the PropertyAccessor instance, either cached or newly created */ private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) { - if (this.nestedPropertyAccessors == null) { - this.nestedPropertyAccessors = new HashMap<>(); + Map nestedAccessors = this.nestedPropertyAccessors; + if (nestedAccessors == null) { + nestedAccessors = new HashMap<>(); + this.nestedPropertyAccessors = nestedAccessors; } // Get value of bean property. PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); @@ -860,7 +852,7 @@ private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nested } // Lookup cached sub-PropertyAccessor, create new one if not found. - AbstractNestablePropertyAccessor nestedPa = this.nestedPropertyAccessors.get(canonicalName); + AbstractNestablePropertyAccessor nestedPa = nestedAccessors.get(canonicalName); if (nestedPa == null || nestedPa.getWrappedInstance() != ObjectUtils.unwrapOptional(value)) { if (logger.isTraceEnabled()) { logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'"); @@ -869,7 +861,7 @@ private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nested // Inherit all type-specific PropertyEditors. copyDefaultEditorsTo(nestedPa); copyCustomEditorsTo(nestedPa, canonicalName); - this.nestedPropertyAccessors.put(canonicalName, nestedPa); + nestedAccessors.put(canonicalName, nestedPa); } else { if (logger.isTraceEnabled()) { @@ -900,16 +892,7 @@ private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) { private Object newValue(Class type, @Nullable TypeDescriptor desc, String name) { try { if (type.isArray()) { - Class componentType = type.componentType(); - // TODO - only handles 2-dimensional arrays - if (componentType.isArray()) { - Object array = Array.newInstance(componentType, 1); - Array.set(array, 0, Array.newInstance(componentType.componentType(), 0)); - return array; - } - else { - return Array.newInstance(componentType, 0); - } + return createArray(type); } else if (Collection.class.isAssignableFrom(type)) { TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null); @@ -933,6 +916,24 @@ else if (Map.class.isAssignableFrom(type)) { } } + /** + * Create the array for the given array type. + * @param arrayType the desired type of the target array + * @return a new array instance + */ + private static Object createArray(Class arrayType) { + Assert.notNull(arrayType, "Array type must not be null"); + Class componentType = arrayType.componentType(); + if (componentType.isArray()) { + Object array = Array.newInstance(componentType, 1); + Array.set(array, 0, createArray(componentType)); + return array; + } + else { + return Array.newInstance(componentType, 0); + } + } + /** * Parse the given property name into the corresponding property name tokens. * @param propertyName the property name to parse @@ -976,11 +977,11 @@ private int getPropertyNameKeyEnd(String propertyName, int startIndex) { int length = propertyName.length(); for (int i = startIndex; i < length; i++) { switch (propertyName.charAt(i)) { - case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR: + case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> { // The property name contains opening prefix(es)... unclosedPrefixes++; - break; - case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR: + } + case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> { if (unclosedPrefixes == 0) { // No unclosed prefix(es) in the property name (left) -> // this is the suffix we are looking for. @@ -991,13 +992,12 @@ private int getPropertyNameKeyEnd(String propertyName, int startIndex) { // just one that occurred within the property name. unclosedPrefixes--; } - break; + } } } return -1; } - @Override public String toString() { String className = getClass().getName(); @@ -1013,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; @@ -1026,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; } @@ -1055,13 +1053,15 @@ 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; + + public boolean setValueFallbackIfPossible(@Nullable Object value) { + return false; + } } @@ -1079,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 3ad632b25439..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,11 +19,11 @@ 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. - * Can be used to plug in custom bean property resolution strategies (e.g. for other + * Can be used to plug in custom bean property resolution strategies (for example, for other * languages on the JVM) or more efficient {@link BeanInfo} retrieval algorithms. * *

BeanInfoFactories are instantiated by the {@link CachedIntrospectionResults}, @@ -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 10bcf80400bf..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; /** @@ -69,7 +67,7 @@ public BeanInstantiationException(Class beanClass, String msg, @Nullable Thro * @param cause the root cause * @since 4.3 */ - public BeanInstantiationException(Constructor constructor, String msg, @Nullable Throwable cause) { + public BeanInstantiationException(Constructor constructor, @Nullable String msg, @Nullable Throwable cause) { super("Failed to instantiate [" + constructor.getDeclaringClass().getName() + "]: " + msg, cause); this.beanClass = constructor.getDeclaringClass(); this.constructor = constructor; @@ -84,7 +82,7 @@ public BeanInstantiationException(Constructor constructor, String msg, @Nulla * @param cause the root cause * @since 4.3 */ - public BeanInstantiationException(Method constructingMethod, String msg, @Nullable Throwable cause) { + public BeanInstantiationException(Method constructingMethod, @Nullable String msg, @Nullable Throwable cause) { super("Failed to instantiate [" + constructingMethod.getReturnType().getName() + "]: " + msg, cause); this.beanClass = constructingMethod.getReturnType(); this.constructor = null; @@ -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 f5c8a854ad48..8c4ad37bc680 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.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,13 +16,15 @@ package org.springframework.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** * Holder for a key-value style attribute that is part of a bean definition. - * Keeps track of the definition source in addition to the key-value pair. + * + *

Keeps track of the definition source in addition to the key-value pair. * * @author Juergen Hoeller * @since 2.5 @@ -31,15 +33,13 @@ 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; /** - * Create a new AttributeValue instance. + * Create a new {@code AttributeValue} instance. * @param name the name of the attribute (never {@code null}) * @param value the value of the attribute (possibly before type conversion) */ @@ -60,8 +60,7 @@ public String getName() { /** * Return the value of the attribute. */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } @@ -74,8 +73,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } @@ -95,7 +93,7 @@ public int hashCode() { @Override public String toString() { - return "metadata attribute '" + this.name + "'"; + return "metadata attribute: name='" + this.name + "'; value=" + this.value; } } 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 70081a97b64e..0da9c8fd525f 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-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.RecordComponent; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; @@ -32,21 +33,19 @@ import java.util.Set; import kotlin.jvm.JvmClassMappingKt; -import kotlin.jvm.JvmInline; import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; -import kotlin.reflect.full.KAnnotatedElements; 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; @@ -127,7 +126,7 @@ public static T instantiate(Class clazz) throws BeanInstantiationExceptio * The cause may notably indicate a {@link NoSuchMethodException} if no * primary/default constructor was found, a {@link NoClassDefFoundError} * or other {@link LinkageError} in case of an unresolvable class definition - * (e.g. due to a missing dependency at runtime), or an exception thrown + * (for example, due to a missing dependency at runtime), or an exception thrown * from the constructor invocation itself. * @see Constructor#newInstance */ @@ -183,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 { @@ -197,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]; @@ -226,9 +225,10 @@ public static T instantiateClass(Constructor ctor, Object... args) throws /** * Return a resolvable constructor for the provided class, either a primary or single - * public constructor with arguments, or a single non-public constructor with arguments, - * or simply a default constructor. Callers have to be prepared to resolve arguments - * for the returned constructor's parameters, if any. + * public constructor with arguments, a single non-public constructor with arguments + * or simply a default constructor. + *

Callers have to be prepared to resolve arguments for the returned constructor's + * parameters, if any. * @param clazz the class to check * @throws IllegalStateException in case of no unique constructor found at all * @since 5.3 @@ -250,7 +250,7 @@ else if (ctors.length == 0) { // No public constructors -> check non-public ctors = clazz.getDeclaredConstructors(); if (ctors.length == 1) { - // A single non-public constructor, e.g. from a non-public record type + // A single non-public constructor, for example, from a non-public record type return (Constructor) ctors[0]; } } @@ -270,18 +270,31 @@ else if (ctors.length == 0) { /** * Return the primary constructor of the provided class. For Kotlin classes, this * returns the Java constructor corresponding to the Kotlin primary constructor - * (as defined in the Kotlin specification). Otherwise, in particular for non-Kotlin - * classes, this simply returns {@code null}. + * (as defined in the Kotlin specification). For Java records, this returns the + * canonical constructor. Otherwise, this simply returns {@code null}. * @param clazz the class to check * @since 5.0 - * @see Kotlin docs + * @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()) { + try { + // Use the canonical constructor which is always present + RecordComponent[] components = clazz.getRecordComponents(); + Class[] paramTypes = new Class[components.length]; + for (int i = 0; i < components.length; i++) { + paramTypes[i] = components[i].getType(); + } + return clazz.getDeclaredConstructor(paramTypes); + } + catch (NoSuchMethodException ignored) { + } + } return null; } @@ -299,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); } @@ -320,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); } @@ -348,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); @@ -371,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); @@ -391,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; @@ -443,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('('); @@ -497,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); } @@ -511,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()); } @@ -526,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) { @@ -540,15 +544,14 @@ public static PropertyDescriptor findPropertyForMethod(Method method, Class c /** * Find a JavaBeans PropertyEditor following the 'Editor' suffix convention - * (e.g. "mypackage.MyDomainClass" → "mypackage.MyDomainClassEditor"). + * (for example, "mypackage.MyDomainClass" → "mypackage.MyDomainClassEditor"). *

Compatible to the standard JavaBeans convention as implemented by * {@link java.beans.PropertyEditorManager} but isolated from the latter's * registered default editors for primitive types. * @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; } @@ -562,7 +565,7 @@ public static PropertyEditor findEditorByConvention(@Nullable Class targetTyp } } catch (Throwable ex) { - // e.g. AccessControlException on Google App Engine + // for example, AccessControlException on Google App Engine return null; } } @@ -595,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); @@ -607,6 +610,22 @@ public static Class findPropertyType(String propertyName, @Nullable Class. return Object.class; } + /** + * Determine whether the specified property has a unique write method, + * i.e. is writable but does not declare overloaded setter methods. + * @param pd the PropertyDescriptor for the property + * @return {@code true} if writable and unique, {@code false} otherwise + * @since 6.1.4 + */ + public static boolean hasUniqueWriteMethod(PropertyDescriptor pd) { + if (pd instanceof GenericTypeAwarePropertyDescriptor gpd) { + return gpd.hasUniqueWriteMethod(); + } + else { + return (pd.getWriteMethod() != null); + } + } + /** * Obtain a new MethodParameter object for the write method of the * specified property. @@ -635,9 +654,10 @@ 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(), () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); @@ -773,7 +793,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"); @@ -850,16 +870,14 @@ 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); if (primaryCtor == null) { return null; } - if (kClass.isValue() && !KAnnotatedElements - .findAnnotations(kClass, JvmClassMappingKt.getKotlinClass(JvmInline.class)).isEmpty()) { + if (KotlinDetector.isInlineClass(clazz)) { Constructor[] constructors = clazz.getDeclaredConstructors(); Assert.state(constructors.length == 1, "Kotlin value classes annotated with @JvmInline are expected to have a single JVM constructor"); @@ -883,7 +901,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); 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 330d20c47130..7171d785214d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.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,9 +19,12 @@ import java.beans.PropertyDescriptor; 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; @@ -61,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; /** @@ -175,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) { @@ -188,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); } @@ -258,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); @@ -278,6 +276,31 @@ public void setValue(@Nullable Object value) throws Exception { ReflectionUtils.makeAccessible(writeMethod); writeMethod.invoke(getWrappedInstance(), value); } + + @Override + public boolean setValueFallbackIfPossible(@Nullable Object value) { + try { + Method writeMethod = this.pd.getWriteMethodFallback(value != null ? value.getClass() : null); + if (writeMethod == null) { + writeMethod = this.pd.getUniqueWriteMethodFallback(); + if (writeMethod != null) { + // Conversion necessary as we would otherwise have received the method + // from the type-matching getWriteMethodFallback call above already + value = convertForProperty(this.pd.getName(), null, value, + new TypeDescriptor(new MethodParameter(writeMethod, 0))); + } + } + if (writeMethod != null) { + ReflectionUtils.makeAccessible(writeMethod); + writeMethod.invoke(getWrappedInstance(), value); + return true; + } + } + catch (Exception ex) { + LogFactory.getLog(BeanPropertyHandler.class).debug("Write method fallback failed", ex); + } + return false; + } } } 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 96d9f94548df..6c9014af84a4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.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,7 +23,6 @@ import java.lang.reflect.Modifier; import java.net.URL; import java.security.ProtectionDomain; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -34,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; @@ -87,8 +86,7 @@ public final class CachedIntrospectionResults { * Set of ClassLoaders that this CachedIntrospectionResults class will always * accept classes from, even if the classes do not qualify as cache-safe. */ - static final Set acceptedClassLoaders = - Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + static final Set acceptedClassLoaders = ConcurrentHashMap.newKeySet(16); /** * Map keyed by Class containing CachedIntrospectionResults, strongly held. @@ -109,7 +107,7 @@ public final class CachedIntrospectionResults { * Accept the given ClassLoader as cache-safe, even if its classes would * not qualify as cache-safe in this CachedIntrospectionResults class. *

This configuration method is only relevant in scenarios where the Spring - * classes reside in a 'common' ClassLoader (e.g. the system ClassLoader) + * classes reside in a 'common' ClassLoader (for example, the system ClassLoader) * whose lifecycle is not coupled to the application. In such a scenario, * CachedIntrospectionResults would by default not cache any of the application's * classes, since they would create a leak in the common ClassLoader. @@ -292,7 +290,7 @@ private CachedIntrospectionResults(Class beanClass) throws BeansException { currClass = currClass.getSuperclass(); } - // Check for record-style accessors without prefix: e.g. "lastName()" + // Check for record-style accessors without prefix: for example, "lastName()" // - accessor method directly referring to instance field of same name // - same convention for component accessors of Java 15 record classes introspectPlainAccessors(beanClass, readMethodNames); @@ -377,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 df2f1333374a..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,12 +36,12 @@ 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; /** - * Decorator for a standard {@link BeanInfo} object, e.g. as created by + * Decorator for a standard {@link BeanInfo} object, for example, as created by * {@link Introspector#getBeanInfo(Class)}, designed to discover and register * static and/or non-void returning setter methods. For example: * @@ -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; } @@ -524,20 +501,7 @@ static class PropertyDescriptorComparator implements Comparator 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 7c6e1d941cb5..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,11 +16,11 @@ package org.springframework.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Thrown on an unrecoverable problem encountered in the - * beans packages or sub-packages, e.g. bad class or field. + * beans packages or sub-packages, for example, bad class or field. * * @author Rod Johnson */ 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 7922f0864dcb..b1f4d72c7b06 100644 --- a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.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. @@ -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,32 +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 volatile Set ambiguousWriteMethods; + private @Nullable Set ambiguousWriteMethods; - @Nullable - private MethodParameter writeMethodParameter; + private volatile boolean ambiguousWriteMethodsLogged; - @Nullable - private volatile ResolvableType writeMethodType; + private @Nullable MethodParameter writeMethodParameter; - @Nullable - private ResolvableType readMethodType; + private volatile @Nullable ResolvableType writeMethodType; - @Nullable - private volatile TypeDescriptor typeDescriptor; + private @Nullable ResolvableType readMethodType; - @Nullable - private Class propertyType; + private volatile @Nullable TypeDescriptor typeDescriptor; - @Nullable - private final Class propertyEditorClass; + private @Nullable Class propertyType; + + private final @Nullable Class propertyEditorClass; public GenericTypeAwarePropertyDescriptor(Class beanClass, String propertyName, @@ -104,9 +97,9 @@ public GenericTypeAwarePropertyDescriptor(Class beanClass, String propertyNam // by the JDK's JavaBeans Introspector... Set ambiguousCandidates = new HashSet<>(); for (Method method : beanClass.getMethods()) { - if (method.getName().equals(writeMethodToUse.getName()) && - !method.equals(writeMethodToUse) && !method.isBridge() && - method.getParameterCount() == writeMethodToUse.getParameterCount()) { + if (method.getName().equals(this.writeMethod.getName()) && + !method.equals(this.writeMethod) && !method.isBridge() && + method.getParameterCount() == this.writeMethod.getParameterCount()) { ambiguousCandidates.add(method); } } @@ -134,29 +127,49 @@ 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; } public Method getWriteMethodForActualAccess() { Assert.state(this.writeMethod != null, "No write method available"); - Set ambiguousCandidates = this.ambiguousWriteMethods; - if (ambiguousCandidates != null) { - this.ambiguousWriteMethods = null; + if (this.ambiguousWriteMethods != null && !this.ambiguousWriteMethodsLogged) { + this.ambiguousWriteMethodsLogged = true; LogFactory.getLog(GenericTypeAwarePropertyDescriptor.class).debug("Non-unique JavaBean property '" + getName() + "' being accessed! Ambiguous write methods found next to actually used [" + - this.writeMethod + "]: " + ambiguousCandidates); + this.writeMethod + "]: " + this.ambiguousWriteMethods); } return this.writeMethod; } + public @Nullable Method getWriteMethodFallback(@Nullable Class valueType) { + if (this.ambiguousWriteMethods != null) { + for (Method method : this.ambiguousWriteMethods) { + Class paramType = method.getParameterTypes()[0]; + if (valueType != null ? paramType.isAssignableFrom(valueType) : !paramType.isPrimitive()) { + return method; + } + } + } + return null; + } + + public @Nullable Method getUniqueWriteMethodFallback() { + if (this.ambiguousWriteMethods != null && this.ambiguousWriteMethods.size() == 1) { + return this.ambiguousWriteMethods.iterator().next(); + } + return null; + } + + public boolean hasUniqueWriteMethod() { + return (this.writeMethod != null && this.ambiguousWriteMethods == null); + } + public MethodParameter getWriteMethodParameter() { Assert.state(this.writeMethodParameter != null, "No write method available"); return this.writeMethodParameter; @@ -187,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 8a0af42cded8..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,11 +18,14 @@ import java.beans.PropertyChangeEvent; +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 { @@ -38,8 +41,10 @@ public class MethodInvocationException extends PropertyAccessException { * @param propertyChangeEvent the PropertyChangeEvent that resulted in an exception * @param cause the Throwable raised by the invoked method */ - public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, Throwable cause) { - super(propertyChangeEvent, "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception", cause); + public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, @Nullable Throwable 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 62543e9a8cf4..b2fe8dfb9bdf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.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. @@ -25,10 +25,10 @@ import java.util.Map; import java.util.Set; import java.util.Spliterator; -import java.util.Spliterators; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -46,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; @@ -255,7 +254,7 @@ public Iterator iterator() { @Override public Spliterator spliterator() { - return Spliterators.spliterator(this.propertyValueList, 0); + return this.propertyValueList.spliterator(); } @Override @@ -269,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; @@ -287,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 564194e2f0aa..73b39e1ca8d1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -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 @@ -91,14 +91,14 @@ private static int getNestedPropertySeparatorIndex(String propertyPath, boolean int i = (last ? length - 1 : 0); while (last ? i >= 0 : i < length) { switch (propertyPath.charAt(i)) { - case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR: - case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR: + case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR, PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> { inKey = !inKey; - break; - case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR: + } + case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR -> { if (!inKey) { return i; } + } } if (last) { i--; @@ -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 9eb05f99d83b..0feea7208617 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.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,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; @@ -68,7 +69,7 @@ public static Collection determineBasicProperties( setter = true; nameIndex = 3; } - else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != Void.TYPE) { + else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != void.class) { setter = false; nameIndex = 3; } @@ -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; @@ -152,7 +152,7 @@ public static Class findPropertyType(@Nullable Method readMethod, @Nullable M throw new IntrospectionException("Bad read method arg count: " + readMethod); } propertyType = readMethod.getReturnType(); - if (propertyType == Void.TYPE) { + if (propertyType == void.class) { throw new IntrospectionException("Read method returns void: " + readMethod); } } @@ -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; @@ -197,11 +196,11 @@ public static Class findIndexedPropertyType(String name, @Nullable Class p if (params.length != 1) { throw new IntrospectionException("Bad indexed read method arg count: " + indexedReadMethod); } - if (params[0] != Integer.TYPE) { + if (params[0] != int.class) { throw new IntrospectionException("Non int index to indexed read method: " + indexedReadMethod); } indexedPropertyType = indexedReadMethod.getReturnType(); - if (indexedPropertyType == Void.TYPE) { + if (indexedPropertyType == void.class) { throw new IntrospectionException("Indexed read method returns void: " + indexedReadMethod); } } @@ -211,7 +210,7 @@ public static Class findIndexedPropertyType(String name, @Nullable Class p if (params.length != 2) { throw new IntrospectionException("Bad indexed write method arg count: " + indexedWriteMethod); } - if (params[0] != Integer.TYPE) { + if (params[0] != int.class) { throw new IntrospectionException("Non int index to indexed write method: " + indexedWriteMethod); } if (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/PropertyEditorRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.java index 69e2a68b3e3c..f96972e81212 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.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. @@ -45,4 +45,18 @@ public interface PropertyEditorRegistrar { */ void registerCustomEditors(PropertyEditorRegistry registry); + /** + * Indicate whether this registrar exclusively overrides default editors + * rather than registering custom editors, intended to be applied lazily. + *

This has an impact on registrar handling in a bean factory: see + * {@link org.springframework.beans.factory.config.ConfigurableBeanFactory#addPropertyEditorRegistrar}. + * @since 6.2.3 + * @see PropertyEditorRegistry#registerCustomEditor + * @see PropertyEditorRegistrySupport#overrideDefaultEditor + * @see PropertyEditorRegistrySupport#setDefaultEditorRegistrar + */ + default boolean overridesDefaultEditors() { + return false; + } + } 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 9843e826d74a..dc07ebd707ff 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,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,27 +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 @Nullable PropertyEditorRegistrar defaultEditorRegistrar; + + @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; /** @@ -126,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; } @@ -155,6 +151,19 @@ public void useConfigValueEditors() { this.configValueEditorsActive = true; } + /** + * Set a registrar for default editors, as a lazy way of overriding default editors. + *

This is expected to be a collaborator with {@link PropertyEditorRegistrySupport}, + * downcasting the given {@link PropertyEditorRegistry} accordingly and calling + * {@link #overrideDefaultEditor} for registering additional default editors on it. + * @param registrar the registrar to call when default editors are actually needed + * @since 6.2.3 + * @see #overrideDefaultEditor + */ + public void setDefaultEditorRegistrar(PropertyEditorRegistrar registrar) { + this.defaultEditorRegistrar = registrar; + } + /** * Override the default editor for the specified type with the given property editor. *

Note that this is different from registering a custom editor in that the editor @@ -178,11 +187,13 @@ public void overrideDefaultEditor(Class requiredType, PropertyEditor property * @return the default editor, or {@code null} if none found * @see #registerDefaultEditors */ - @Nullable - public PropertyEditor getDefaultEditor(Class requiredType) { + public @Nullable PropertyEditor getDefaultEditor(Class requiredType) { if (!this.defaultEditorsActive) { return null; } + if (this.overriddenDefaultEditors == null && this.defaultEditorRegistrar != null) { + this.defaultEditorRegistrar.registerCustomEditors(this); + } if (this.overriddenDefaultEditors != null) { PropertyEditor editor = this.overriddenDefaultEditors.get(requiredType); if (editor != null) { @@ -311,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) { @@ -371,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; } @@ -382,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); @@ -397,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; } @@ -437,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) { @@ -525,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; @@ -537,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 a3bfd17bc169..54aaa607f075 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,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; @@ -49,6 +49,7 @@ * @author Juergen Hoeller * @author Rob Harrop * @author Dave Syer + * @author Yanming Zhou * @since 2.0 * @see BeanWrapperImpl * @see SimpleTypeConverter @@ -59,8 +60,7 @@ class TypeConverterDelegate { private final PropertyEditorRegistrySupport propertyEditorRegistry; - @Nullable - private final Object targetObject; + private final @Nullable Object targetObject; /** @@ -92,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)); @@ -112,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? @@ -178,15 +176,15 @@ else if (requiredType.isArray()) { return (T) convertToTypedArray(convertedValue, propertyName, requiredType.componentType()); } else if (convertedValue.getClass().isArray()) { - if (Array.getLength(convertedValue) == 1) { - convertedValue = Array.get(convertedValue, 0); - standardConversion = true; - } - else if (Collection.class.isAssignableFrom(requiredType)) { + if (Collection.class.isAssignableFrom(requiredType)) { convertedValue = convertToTypedCollection(CollectionUtils.arrayToList(convertedValue), propertyName, requiredType, typeDescriptor); standardConversion = true; } + else if (Array.getLength(convertedValue) == 1) { + convertedValue = Array.get(convertedValue, 0); + standardConversion = true; + } } else if (convertedValue instanceof Collection coll) { // Convert elements to target type, if determined. @@ -314,7 +312,7 @@ private Object attemptToConvertStringToEnum(Class requiredType, String trimme } if (convertedValue == currentConvertedValue) { - // Try field lookup as fallback: for JDK 1.5 enum or custom enum + // Try field lookup as fallback: for Java enum or custom enum // with values defined as static fields. Resulting value still needs // to be checked, hence we don't return it right away. try { @@ -336,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. @@ -361,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; @@ -627,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 cac597c28ec7..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; /** @@ -94,7 +92,7 @@ public BeanCreationException(String beanName, String msg, Throwable cause) { * @param beanName the name of the bean requested * @param msg the detail message */ - public BeanCreationException(@Nullable String resourceDescription, @Nullable String beanName, String msg) { + public BeanCreationException(@Nullable String resourceDescription, @Nullable String beanName, @Nullable String msg) { super("Error creating bean with name '" + beanName + "'" + (resourceDescription != null ? " defined in " + resourceDescription : "") + ": " + msg); this.resourceDescription = resourceDescription; @@ -110,7 +108,7 @@ public BeanCreationException(@Nullable String resourceDescription, @Nullable Str * @param msg the detail message * @param cause the root cause */ - public BeanCreationException(@Nullable String resourceDescription, String beanName, String msg, Throwable cause) { + public BeanCreationException(@Nullable String resourceDescription, String beanName, @Nullable String msg, Throwable cause) { this(resourceDescription, beanName, msg); initCause(cause); } @@ -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/BeanCurrentlyInCreationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java index 4c984fb12784..5f5fc7b99d30 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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. @@ -32,8 +32,8 @@ public class BeanCurrentlyInCreationException extends BeanCreationException { * @param beanName the name of the bean requested */ public BeanCurrentlyInCreationException(String beanName) { - super(beanName, - "Requested bean is currently in creation: Is there an unresolvable circular reference?"); + super(beanName, "Requested bean is currently in creation: "+ + "Is there an unresolvable circular reference or an asynchronous initialization dependency?"); } /** 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 d807d5f90179..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,12 +16,13 @@ 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: - * e.g. in case of incomplete or contradictory bean metadata. + * for example, in case of incomplete or contradictory bean metadata. * * @author Rod Johnson * @author Juergen Hoeller @@ -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 696a9c1cd59e..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. @@ -36,7 +37,7 @@ * singleton in the scope of the factory). Which type of instance will be returned * depends on the bean factory configuration: the API is the same. Since Spring * 2.0, further scopes are available depending on the concrete application - * context (e.g. "request" and "session" scopes in a web environment). + * context (for example, "request" and "session" scopes in a web environment). * *

The point of this approach is that the BeanFactory is a central registry * of application components, and centralizes configuration of application @@ -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. @@ -170,6 +178,8 @@ public interface BeanFactory { * Return an instance, which may be shared or independent, of the specified bean. *

Allows for specifying explicit constructor arguments / factory method arguments, * overriding the specified default arguments (if any) in the bean definition. + * Note that the provided arguments need to match a specific candidate constructor / + * factory method in the order of declared parameters. * @param name the name of the bean to retrieve * @param args arguments to use when creating a bean instance using explicit arguments * (only applied when creating a new instance as opposed to retrieving an existing one) @@ -180,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. @@ -202,6 +212,8 @@ public interface BeanFactory { * Return an instance, which may be shared or independent, of the specified bean. *

Allows for specifying explicit constructor arguments / factory method arguments, * overriding the specified default arguments (if any) in the bean definition. + * Note that the provided arguments need to match a specific candidate constructor / + * factory method in the order of declared parameters. *

This method goes into {@link ListableBeanFactory} by-type lookup territory * but may also be translated into a conventional by-name lookup based on the name * of the given type. For more extensive retrieval operations across sets of beans, @@ -216,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 @@ -239,7 +251,7 @@ public interface BeanFactory { * 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´ + * For lenient fallback matching with unchecked semantics (similar to the 'unchecked' * Java compiler warning), consider calling {@link #getBeanProvider(Class)} with the * raw type as a second step if no full generic match is * {@link ObjectProvider#getIfAvailable() available} with this variant. @@ -353,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, @@ -374,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/BeanFactoryInitializer.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryInitializer.java new file mode 100644 index 000000000000..30c08739bbbd --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryInitializer.java @@ -0,0 +1,41 @@ +/* + * 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.beans.factory; + +/** + * Callback interface for initializing a Spring {@link ListableBeanFactory} + * prior to entering the singleton pre-instantiation phase. Can be used to + * trigger early initialization of specific beans before regular singletons. + * + *

Can be programmatically applied to a {@code ListableBeanFactory} instance. + * In an {@code ApplicationContext}, beans of type {@code BeanFactoryInitializer} + * will be autodetected and automatically applied to the underlying bean factory. + * + * @author Juergen Hoeller + * @since 6.2 + * @param the bean factory type + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons() + */ +public interface BeanFactoryInitializer { + + /** + * Initialize the given bean factory. + * @param beanFactory the bean factory to bootstrap + */ + void initialize(F beanFactory); + +} 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 079760177033..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 @@ -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,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; @@ -39,10 +40,14 @@ * (which the methods defined on the ListableBeanFactory interface don't, * in contrast to the methods defined on the BeanFactory interface). * + *

NOTE: It is generally preferable to use {@link ObjectProvider#stream()} + * via {@link BeanFactory#getBeanProvider} instead of this utility class. + * * @author Rod Johnson * @author Juergen Hoeller * @author Chris Beams * @since 04.07.2003 + * @see BeanFactory#getBeanProvider */ public abstract class BeanFactoryUtils { @@ -68,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); } /** @@ -80,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; }); } @@ -308,7 +313,7 @@ public static String[] beanNamesForAnnotationIncludingAncestors( * 'replacing' beans by explicitly choosing the same bean name in a child factory; * the bean in the ancestor factory won't be visible then, not even for by-type lookups. * @param lbf the bean factory - * @param type type of bean to match + * @param type the type of bean to match * @return the Map of matching bean instances, or an empty Map if none * @throws BeansException if a bean could not be created * @see ListableBeanFactory#getBeansOfType(Class) @@ -347,7 +352,7 @@ public static Map beansOfTypeIncludingAncestors(ListableBeanFacto * 'replacing' beans by explicitly choosing the same bean name in a child factory; * the bean in the ancestor factory won't be visible then, not even for by-type lookups. * @param lbf the bean factory - * @param type type of bean to match + * @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 @@ -395,7 +400,7 @@ public static Map beansOfTypeIncludingAncestors( * 'replacing' beans by explicitly choosing the same bean name in a child factory; * the bean in the ancestor factory won't be visible then, not even for by-type lookups. * @param lbf the bean factory - * @param type type of bean to match + * @param type the type of bean to match * @return the matching bean instance * @throws NoSuchBeanDefinitionException if no bean of the given type was found * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found @@ -425,7 +430,7 @@ public static T beanOfTypeIncludingAncestors(ListableBeanFactory lbf, Class< * 'replacing' beans by explicitly choosing the same bean name in a child factory; * the bean in the ancestor factory won't be visible then, not even for by-type lookups. * @param lbf the bean factory - * @param type type of bean to match + * @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 @@ -457,7 +462,7 @@ public static T beanOfTypeIncludingAncestors( *

This version of {@code beanOfType} automatically includes * prototypes and FactoryBeans. * @param lbf the bean factory - * @param type type of bean to match + * @param type the type of bean to match * @return the matching bean instance * @throws NoSuchBeanDefinitionException if no bean of the given type was found * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found @@ -481,7 +486,7 @@ public static T beanOfType(ListableBeanFactory lbf, Class type) throws Be * only raw FactoryBeans will be checked (which doesn't require initialization * of each FactoryBean). * @param lbf the bean factory - * @param type type of bean to match + * @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 @@ -529,7 +534,7 @@ private static String[] mergeNamesWithParent(String[] result, String[] parentRes /** * Extract a unique bean for the given type from the given Map of matching beans. - * @param type type of bean to match + * @param type the type of bean to match * @param matchingBeans all matching beans found * @return the unique bean instance * @throws NoSuchBeanDefinitionException if no bean of the given type was found 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..5a81d6f8e637 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.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.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 some + * other 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 97362ce1f7c9..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 @@ -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,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 @@ -39,9 +39,9 @@ * *

{@code FactoryBean} is a programmatic contract. Implementations are not * supposed to rely on annotation-driven injection or other reflective facilities. - * {@link #getObjectType()} {@link #getObject()} invocations may arrive early in the - * bootstrap process, even ahead of any post-processor setup. If you need access to - * other beans, implement {@link BeanFactoryAware} and obtain them programmatically. + * Invocations of {@link #getObjectType()} and {@link #getObject()} may arrive early + * in the bootstrap process, even ahead of any post-processor setup. If you need access + * to other beans, implement {@link BeanFactoryAware} and obtain them programmatically. * *

The container is only responsible for managing the lifecycle of the FactoryBean * instance, not the lifecycle of the objects created by the FactoryBean. Therefore, @@ -50,7 +50,7 @@ * {@link DisposableBean} and delegate any such close call to the underlying object. * *

Finally, FactoryBean objects participate in the containing BeanFactory's - * synchronization of bean creation. There is usually no need for internal + * synchronization of bean creation. Thus, there is usually no need for internal * synchronization other than for purposes of lazy initialization within the * FactoryBean itself (or the like). * @@ -68,7 +68,7 @@ public interface FactoryBean { * The name of an attribute that can be * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a * {@link org.springframework.beans.factory.config.BeanDefinition} so that - * factory beans can signal their object type when it can't be deduced from + * factory beans can signal their object type when it cannot be deduced from * the factory bean class. * @since 5.2 */ @@ -79,28 +79,27 @@ public interface FactoryBean { * Return an instance (possibly shared or independent) of the object * managed by this factory. *

As with a {@link BeanFactory}, this allows support for both the - * Singleton and Prototype design pattern. + * Singleton and Prototype design patterns. *

If this FactoryBean is not fully initialized yet at the time of * the call (for example because it is involved in a circular reference), * throw a corresponding {@link FactoryBeanNotInitializedException}. - *

As of Spring 2.0, FactoryBeans are allowed to return {@code null} - * objects. The factory will consider this as normal value to be used; it - * will not throw a FactoryBeanNotInitializedException in this case anymore. + *

FactoryBeans are allowed to return {@code null} objects. The bean + * factory will consider this as a normal value to be used and will not throw + * a {@code FactoryBeanNotInitializedException} in this case. However, * FactoryBean implementations are encouraged to throw - * FactoryBeanNotInitializedException themselves now, as appropriate. + * {@code FactoryBeanNotInitializedException} themselves, as appropriate. * @return an instance of the bean (can be {@code null}) * @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, * or {@code null} if not known in advance. *

This allows one to check for specific types of beans without * instantiating objects, for example on autowiring. - *

In the case of implementations that are creating a singleton object, + *

In the case of implementations that create a singleton object, * this method should try to avoid singleton creation as far as possible; * it should rather estimate the type in advance. * For prototypes, returning a meaningful type here is advisable too. @@ -114,15 +113,14 @@ 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, * will {@link #getObject()} always return the same object * (a reference that can be cached)? - *

NOTE: If a FactoryBean indicates to hold a singleton object, - * the object returned from {@code getObject()} might get cached + *

NOTE: If a FactoryBean indicates that it holds a singleton + * object, the object returned from {@code getObject()} might get cached * by the owning BeanFactory. Hence, do not return {@code true} * unless the FactoryBean always exposes the same reference. *

The singleton status of the FactoryBean itself will generally 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/InitializingBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/InitializingBean.java index 940c2dd922a9..a062dc9e1aa9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/InitializingBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/InitializingBean.java @@ -18,7 +18,7 @@ /** * Interface to be implemented by beans that need to react once all their properties - * have been set by a {@link BeanFactory}: e.g. to perform custom initialization, + * have been set by a {@link BeanFactory}: for example, to perform custom initialization, * or merely to check that all mandatory properties have been set. * *

An alternative to implementing {@code InitializingBean} is specifying a custom 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 0a4731f904c5..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 @@ -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,16 +22,19 @@ 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; /** * A simple descriptor for an injection point, pointing to a method/constructor - * parameter or a field. Exposed by {@link UnsatisfiedDependencyException}. - * Also available as an argument for factory methods, reacting to the - * requesting injection point for building a customized bean instance. + * parameter or a field. + * + *

Exposed by {@link UnsatisfiedDependencyException}. Also available as an + * argument for factory methods, reacting to the requesting injection point + * for building a customized bean instance. * * @author Juergen Hoeller * @since 4.3 @@ -40,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; /** @@ -91,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; } @@ -101,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; } @@ -140,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 edb0381dbd00..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-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,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 @@ -92,9 +93,13 @@ public interface ListableBeanFactory extends BeanFactory { * Return a provider for the specified bean, allowing for lazy on-demand retrieval * of instances, including availability and uniqueness options. * @param requiredType type the bean must match; can be an interface or superclass - * @param allowEagerInit whether stream-based access may initialize lazy-init - * singletons and objects created by FactoryBeans (or by factory methods - * with a "factory-bean" reference) for the type check + * @param allowEagerInit whether stream access may introspect 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. Only + * actually necessary initialization for type checking purposes will be performed; + * constructor and method invocations will still be avoided as far as possible. * @return a corresponding provider handle * @since 5.3 * @see #getBeanProvider(ResolvableType, boolean) @@ -112,9 +117,13 @@ public interface ListableBeanFactory extends BeanFactory { * 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. - * @param allowEagerInit whether stream-based access may initialize lazy-init - * singletons and objects created by FactoryBeans (or by factory methods - * with a "factory-bean" reference) for the type check + * @param allowEagerInit whether stream access may introspect 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. Only + * actually necessary initialization for type checking purposes will be performed; + * constructor and method invocations will still be avoided as far as possible. * @return a corresponding provider handle * @since 5.3 * @see #getBeanProvider(ResolvableType) @@ -137,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)}. @@ -168,18 +175,18 @@ 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 * @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 + * @param allowEagerInit whether to introspect 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. + * for this flag will initialize FactoryBeans and "factory-bean" references. Only + * actually necessary initialization for type checking purposes will be performed; + * constructor and method invocations will still be avoided as far as possible. * @return the names of beans (or objects created by FactoryBeans) matching * the given object type (including subclasses), or an empty array if none * @since 5.2 @@ -200,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)}. @@ -229,18 +234,18 @@ 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 * @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 + * @param allowEagerInit whether to introspect 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. + * for this flag will initialize FactoryBeans and "factory-bean" references. Only + * actually necessary initialization for type checking purposes will be performed; + * constructor and method invocations will still be avoided as far as possible. * @return the names of beans (or objects created by FactoryBeans) matching * the given object type (including subclasses), or an empty array if none * @see FactoryBean#getObjectType @@ -253,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 @@ -283,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 @@ -292,19 +302,22 @@ 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) - * @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 + * @param allowEagerInit whether to introspect 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. + * for this flag will initialize FactoryBeans and "factory-bean" references. Only + * actually necessary initialization for type checking purposes will be performed; + * constructor and method invocations will still be avoided as far as possible. * @return a Map with the matching beans, containing the bean names as * keys and the corresponding bean instances as values * @throws BeansException if a bean could not be created @@ -361,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; /** @@ -383,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 9e30f2f72c59..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 @@ -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,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; /** @@ -29,6 +30,7 @@ * multiple matching candidates have been found when only one matching bean was expected. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.2.1 * @see BeanFactory#getBean(Class) */ @@ -37,10 +39,22 @@ public class NoUniqueBeanDefinitionException extends NoSuchBeanDefinitionExcepti private final int numberOfBeansFound; - @Nullable - private final Collection beanNamesFound; + private final @Nullable Collection beanNamesFound; + /** + * Create a new {@code NoUniqueBeanDefinitionException}. + * @param type required type of the non-unique bean + * @param beanNamesFound the names of all matching beans (as a Collection) + * @param message detailed message describing the problem + * @since 6.2 + */ + public NoUniqueBeanDefinitionException(Class type, Collection beanNamesFound, String message) { + super(type, message); + this.numberOfBeansFound = beanNamesFound.size(); + this.beanNamesFound = new ArrayList<>(beanNamesFound); + } + /** * Create a new {@code NoUniqueBeanDefinitionException}. * @param type required type of the non-unique bean @@ -59,10 +73,8 @@ public NoUniqueBeanDefinitionException(Class type, int numberOfBeansFound, St * @param beanNamesFound the names of all matching beans (as a Collection) */ public NoUniqueBeanDefinitionException(Class type, Collection beanNamesFound) { - super(type, "expected single matching bean but found " + beanNamesFound.size() + ": " + + this(type, beanNamesFound, "expected single matching bean but found " + beanNamesFound.size() + ": " + StringUtils.collectionToCommaDelimitedString(beanNamesFound)); - this.numberOfBeansFound = beanNamesFound.size(); - this.beanNamesFound = new ArrayList<>(beanNamesFound); } /** @@ -114,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 a9dc61eea426..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 @@ -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,20 +18,50 @@ import java.util.Iterator; import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; -import org.springframework.lang.Nullable; +import org.springframework.core.OrderComparator; /** * A variant of {@link ObjectFactory} designed specifically for injection points, * allowing for programmatic optionality and lenient not-unique handling. * + *

In a {@link BeanFactory} environment, every {@code ObjectProvider} obtained + * from the factory will be bound to its {@code BeanFactory} for a specific bean + * type, matching all provider calls against factory-registered bean definitions. + * Note that all such calls dynamically operate on the underlying factory state, + * freshly resolving the requested target object on every call. + * *

As of 5.1, this interface extends {@link Iterable} and provides {@link Stream} * support. It can be therefore be used in {@code for} loops, provides {@link #forEach} * iteration and allows for collection-style {@link #stream} access. * + *

As of 6.2, this interface declares default implementations for all methods. + * This makes it easier to implement in a custom fashion, for example, for unit tests. + * For typical purposes, implement {@link #stream()} to enable all other methods. + * Alternatively, you may implement the specific methods that your callers expect, + * for example, just {@link #getObject()} or {@link #getIfAvailable()}. + * + *

Note that {@link #getObject()} never returns {@code null} - it will throw a + * {@link NoSuchBeanDefinitionException} instead -, whereas {@link #getIfAvailable()} + * will return {@code null} if no matching bean is present at all. However, both + * methods will throw a {@link NoUniqueBeanDefinitionException} if more than one + * matching bean is found without a clear unique winner (see below). Last but not + * least, {@link #getIfUnique()} will return {@code null} both when no matching bean + * is found and when more than one matching bean is found without a unique winner. + * + *

Uniqueness is generally up to the container's candidate resolution algorithm + * but always honors the "primary" flag (with only one of the candidate beans marked + * as primary) and the "fallback" flag (with only one of the candidate beans not + * marked as fallback). The default-candidate flag is consistently taken into + * account as well, even for non-annotation-based injection points, with a single + * default candidate winning in case of no clear primary/fallback indication. + * * @author Juergen Hoeller * @since 4.3 * @param the object type @@ -40,6 +70,31 @@ */ public interface ObjectProvider extends ObjectFactory, Iterable { + /** + * A predicate for unfiltered type matches, including non-default candidates + * but still excluding non-autowire candidates when used on injection points. + * @since 6.2.3 + * @see #stream(Predicate) + * @see #orderedStream(Predicate) + * @see org.springframework.beans.factory.config.BeanDefinition#isAutowireCandidate() + * @see org.springframework.beans.factory.support.AbstractBeanDefinition#isDefaultCandidate() + */ + Predicate> UNFILTERED = (clazz -> true); + + + @Override + default T getObject() throws BeansException { + Iterator it = iterator(); + if (!it.hasNext()) { + throw new NoSuchBeanDefinitionException(Object.class); + } + T result = it.next(); + if (it.hasNext()) { + throw new NoUniqueBeanDefinitionException(Object.class, 2, "more than 1 matching bean"); + } + return result; + } + /** * Return an instance (possibly shared or independent) of the object * managed by this factory. @@ -50,7 +105,10 @@ public interface ObjectProvider extends ObjectFactory, Iterable { * @throws BeansException in case of creation errors * @see #getObject() */ - 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"); + } /** * Return an instance (possibly shared or independent) of the object @@ -59,8 +117,17 @@ public interface ObjectProvider extends ObjectFactory, Iterable { * @throws BeansException in case of creation errors * @see #getObject() */ - @Nullable - T getIfAvailable() throws BeansException; + default @Nullable T getIfAvailable() throws BeansException { + try { + return getObject(); + } + catch (NoUniqueBeanDefinitionException ex) { + throw ex; + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } /** * Return an instance (possibly shared or independent) of the object @@ -102,8 +169,14 @@ default void ifAvailable(Consumer dependencyConsumer) throws BeansException { * @throws BeansException in case of creation errors * @see #getObject() */ - @Nullable - T getIfUnique() throws BeansException; + default @Nullable T getIfUnique() throws BeansException { + try { + return getObject(); + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } /** * Return an instance (possibly shared or independent) of the object @@ -129,7 +202,7 @@ default T getIfUnique(Supplier defaultSupplier) throws BeansException { * if unique (not called otherwise) * @throws BeansException in case of creation errors * @since 5.0 - * @see #getIfAvailable() + * @see #getIfUnique() */ default void ifUnique(Consumer dependencyConsumer) throws BeansException { T dependency = getIfUnique(); @@ -152,12 +225,17 @@ default Iterator iterator() { /** * Return a sequential {@link Stream} over all matching object instances, * without specific ordering guarantees (but typically in registration order). + *

Note: The result may be filtered by default according to qualifiers on the + * injection point versus target beans and the general autowire candidate status + * of matching beans. For custom filtering against type-matching candidates, use + * {@link #stream(Predicate)} instead (potentially with {@link #UNFILTERED}). * @since 5.1 * @see #iterator() * @see #orderedStream() */ default Stream stream() { - throw new UnsupportedOperationException("Multi element access not supported"); + throw new UnsupportedOperationException("Element access not supported - " + + "for custom ObjectProvider classes, implement stream() to enable all other methods"); } /** @@ -168,12 +246,86 @@ default Stream stream() { * and in case of annotation-based configuration also considering the * {@link org.springframework.core.annotation.Order} annotation, * analogous to multi-element injection points of list/array type. + *

The default method applies an {@link OrderComparator} to the + * {@link #stream()} method. You may override this to apply an + * {@link org.springframework.core.annotation.AnnotationAwareOrderComparator} + * if necessary. + *

Note: The result may be filtered by default according to qualifiers on the + * injection point versus target beans and the general autowire candidate status + * of matching beans. For custom filtering against type-matching candidates, use + * {@link #stream(Predicate)} instead (potentially with {@link #UNFILTERED}). * @since 5.1 * @see #stream() * @see org.springframework.core.OrderComparator */ default Stream orderedStream() { - throw new UnsupportedOperationException("Ordered element access not supported"); + return stream().sorted(OrderComparator.INSTANCE); + } + + /** + * 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) + * @since 6.2.3 + * @see #stream() + * @see #orderedStream(Predicate) + */ + default Stream stream(Predicate> customFilter) { + return stream(customFilter, true); + } + + /** + * 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) + * @since 6.2.3 + * @see #orderedStream() + * @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/SmartInitializingSingleton.java b/spring-beans/src/main/java/org/springframework/beans/factory/SmartInitializingSingleton.java index 3df636346b9c..fa26c908f18c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/SmartInitializingSingleton.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/SmartInitializingSingleton.java @@ -21,7 +21,7 @@ * during {@link BeanFactory} bootstrap. This interface can be implemented by * singleton beans in order to perform some initialization after the regular * singleton instantiation algorithm, avoiding side effects with accidental early - * initialization (e.g. from {@link ListableBeanFactory#getBeansOfType} calls). + * initialization (for example, from {@link ListableBeanFactory#getBeansOfType} calls). * In that sense, it is an alternative to {@link InitializingBean} which gets * triggered right at the end of a bean's local construction phase. * 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 29c4b47349f8..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; /** @@ -44,7 +44,7 @@ public class UnsatisfiedDependencyException extends BeanCreationException { * @param msg the detail message */ public UnsatisfiedDependencyException( - @Nullable String resourceDescription, @Nullable String beanName, String propertyName, String msg) { + @Nullable String resourceDescription, @Nullable String beanName, String propertyName, @Nullable String msg) { super(resourceDescription, beanName, "Unsatisfied dependency expressed through bean property '" + propertyName + "'" + @@ -75,7 +75,7 @@ public UnsatisfiedDependencyException( * @since 4.3 */ public UnsatisfiedDependencyException( - @Nullable String resourceDescription, @Nullable String beanName, @Nullable InjectionPoint injectionPoint, String msg) { + @Nullable String resourceDescription, @Nullable String beanName, @Nullable InjectionPoint injectionPoint, @Nullable String msg) { super(resourceDescription, beanName, "Unsatisfied dependency expressed through " + 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/Autowired.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.java index 0fdc535ec4b2..0ba5e9f79890 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.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. @@ -54,18 +54,17 @@ * *

Autowired Parameters

*

Although {@code @Autowired} can technically be declared on individual method - * or constructor parameters since Spring Framework 5.0, most parts of the - * framework ignore such declarations. The only part of the core Spring Framework - * that actively supports autowired parameters is the JUnit Jupiter support in - * the {@code spring-test} module (see the + * or constructor parameters, most parts of the framework ignore such declarations. + * The only part of the core Spring Framework that actively supports autowired + * parameters is the JUnit Jupiter support in the {@code spring-test} module (see the * TestContext framework * reference documentation for details). * *

Multiple Arguments and 'required' Semantics

*

In the case of a multi-arg constructor or method, the {@link #required} attribute * is applicable to all arguments. Individual parameters may be declared as Java-8 style - * {@link java.util.Optional} or, as of Spring Framework 5.0, also as {@code @Nullable} - * or a not-null parameter type in Kotlin, overriding the base 'required' semantics. + * {@link java.util.Optional} as well as {@code @Nullable} or a not-null parameter + * type in Kotlin, overriding the base 'required' semantics. * *

Autowiring Arrays, Collections, and Maps

*

In case of an array, {@link java.util.Collection}, or {@link java.util.Map} 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 94a80f3b59e6..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-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,7 +29,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -39,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; @@ -63,6 +63,7 @@ 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.aot.CodeWarnings; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; @@ -83,12 +84,11 @@ 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; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -102,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 @@ -159,9 +157,12 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, BeanRegistrationAotProcessor, PriorityOrdered, BeanFactoryAware { + private static final Constructor[] EMPTY_CONSTRUCTOR_ARRAY = new Constructor[0]; + + protected final Log logger = LogFactory.getLog(getClass()); - private final Set> autowiredAnnotationTypes = new LinkedHashSet<>(4); + private final Set> autowiredAnnotationTypes = CollectionUtils.newLinkedHashSet(4); private String requiredParameterName = "required"; @@ -169,13 +170,11 @@ 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 = Collections.newSetFromMap(new ConcurrentHashMap<>(256)); + private final Set lookupMethodsChecked = ConcurrentHashMap.newKeySet(256); private final Map, Constructor[]> candidateConstructorsCache = new ConcurrentHashMap<>(256); @@ -185,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() { @@ -202,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. - } } @@ -280,18 +270,35 @@ 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()); } @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + // Register externally managed config members on bean definition. findInjectionMetadata(beanName, beanType, beanDefinition); + + // Use opportunity to clear caches which are not needed after singleton instantiation. + // The injectionMetadataCache itself is left intact since it cannot be reliably + // reconstructed in terms of externally managed config members otherwise. + if (beanDefinition.isSingleton()) { + this.candidateConstructorsCache.remove(beanType); + // With actual lookup overrides, keep it intact along with bean definition. + if (!beanDefinition.hasMethodOverrides()) { + this.lookupMethodsChecked.remove(beanName); + } + } } @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public void resetBeanDefinition(String beanName) { + this.lookupMethodsChecked.remove(beanName); + this.injectionMetadataCache.remove(beanName); + } + + @Override + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); String beanName = registeredBean.getBeanName(); RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); @@ -310,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(); } @@ -324,12 +330,6 @@ private InjectionMetadata findInjectionMetadata(String beanName, Class beanTy return metadata; } - @Override - public void resetBeanDefinition(String beanName) { - this.lookupMethodsChecked.remove(beanName); - this.injectionMetadataCache.remove(beanName); - } - @Override public Class determineBeanType(Class beanClass, String beanName) throws BeanCreationException { checkLookupMethods(beanClass, beanName); @@ -345,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); @@ -429,7 +428,7 @@ else if (candidates.size() == 1 && logger.isInfoEnabled()) { "default constructor to fall back to: " + candidates.get(0)); } } - candidateConstructors = candidates.toArray(new Constructor[0]); + candidateConstructors = candidates.toArray(EMPTY_CONSTRUCTOR_ARRAY); } else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) { candidateConstructors = new Constructor[] {rawCandidates[0]}; @@ -442,7 +441,7 @@ else if (nonSyntheticConstructors == 1 && primaryConstructor != null) { candidateConstructors = new Constructor[] {primaryConstructor}; } else { - candidateConstructors = new Constructor[0]; + candidateConstructors = EMPTY_CONSTRUCTOR_ARRAY; } this.candidateConstructorsCache.put(beanClass, candidateConstructors); } @@ -502,9 +501,9 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str } /** - * 'Native' processing method for direct calls with an arbitrary target instance, - * resolving all of its fields and methods which are annotated with one of the - * configured 'autowired' annotation types. + * Native processing method for direct calls with an arbitrary target + * instance, resolving all of its fields and methods which are annotated with + * one of the configured 'autowired' annotation types. * @param bean the target instance to process * @throws BeanCreationException if autowiring failed * @see #setAutowiredAnnotationTypes(Set) @@ -607,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); @@ -693,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); @@ -726,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); @@ -757,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); @@ -804,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); @@ -817,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); @@ -843,24 +837,22 @@ 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 = new LinkedHashSet<>(argumentCount * 2); + Set autowiredBeanNames = CollectionUtils.newLinkedHashSet(argumentCount); Assert.state(beanFactory != null, "No BeanFactory available"); TypeConverter typeConverter = beanFactory.getTypeConverter(); for (int i = 0; i < arguments.length; i++) { @@ -944,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) { @@ -969,8 +960,11 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER); method.addParameter(this.target, INSTANCE_PARAMETER); method.returns(this.target); - method.addCode(generateMethodCode(generatedClass.getName(), - generationContext.getRuntimeHints())); + CodeWarnings codeWarnings = new CodeWarnings(); + codeWarnings.detectDeprecation(this.target); + method.addCode(generateMethodCode(codeWarnings, + generatedClass.getName(), generationContext.getRuntimeHints())); + codeWarnings.suppress(method); }); beanRegistrationCode.addInstancePostProcessor(generateMethod.toMethodReference()); @@ -979,35 +973,37 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be } } - private CodeBlock generateMethodCode(ClassName targetClassName, RuntimeHints hints) { + private CodeBlock generateMethodCode(CodeWarnings codeWarnings, + ClassName targetClassName, RuntimeHints hints) { + CodeBlock.Builder code = CodeBlock.builder(); for (AutowiredElement autowiredElement : this.autowiredElements) { code.addStatement(generateMethodStatementForElement( - targetClassName, autowiredElement, hints)); + codeWarnings, targetClassName, autowiredElement, hints)); } code.addStatement("return $L", INSTANCE_PARAMETER); return code.build(); } - private CodeBlock generateMethodStatementForElement(ClassName targetClassName, - AutowiredElement autowiredElement, RuntimeHints hints) { + private CodeBlock generateMethodStatementForElement(CodeWarnings codeWarnings, + ClassName targetClassName, AutowiredElement autowiredElement, RuntimeHints hints) { Member member = autowiredElement.getMember(); boolean required = autowiredElement.required; if (member instanceof Field field) { return generateMethodStatementForField( - targetClassName, field, required, hints); + codeWarnings, targetClassName, field, required, hints); } if (member instanceof Method method) { return generateMethodStatementForMethod( - targetClassName, method, required, hints); + codeWarnings, targetClassName, method, required, hints); } throw new IllegalStateException( "Unsupported member type " + member.getClass().getName()); } - private CodeBlock generateMethodStatementForField(ClassName targetClassName, - Field field, boolean required, RuntimeHints hints) { + private CodeBlock generateMethodStatementForField(CodeWarnings codeWarnings, + ClassName targetClassName, Field field, boolean required, RuntimeHints hints) { hints.reflection().registerField(field); CodeBlock resolver = CodeBlock.of("$T.$L($S)", @@ -1018,18 +1014,22 @@ private CodeBlock generateMethodStatementForField(ClassName targetClassName, return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); } - return CodeBlock.of("$L.$L = $L.resolve($L)", INSTANCE_PARAMETER, - field.getName(), resolver, REGISTERED_BEAN_PARAMETER); + else { + codeWarnings.detectDeprecation(field); + return CodeBlock.of("$L.$L = $L.resolve($L)", INSTANCE_PARAMETER, + field.getName(), resolver, REGISTERED_BEAN_PARAMETER); + } } - private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, - Method method, boolean required, RuntimeHints hints) { + private CodeBlock generateMethodStatementForMethod(CodeWarnings codeWarnings, + ClassName targetClassName, Method method, boolean required, RuntimeHints hints) { CodeBlock.Builder code = CodeBlock.builder(); code.add("$T.$L", AutowiredMethodArgumentsResolver.class, (!required ? "forMethod" : "forRequiredMethod")); code.add("($S", method.getName()); if (method.getParameterCount() > 0) { + codeWarnings.detectDeprecation(method.getParameterTypes()); code.add(", $L", generateParameterTypesCode(method.getParameterTypes())); } code.add(")"); @@ -1039,7 +1039,8 @@ private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, code.add(".resolveAndInvoke($L, $L)", REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); } else { - hints.reflection().registerMethod(method, ExecutableMode.INTROSPECT); + codeWarnings.detectDeprecation(method); + hints.reflection().registerType(method.getDeclaringClass()); CodeBlock arguments = new AutowiredArgumentsCodeGenerator(this.target, method).generateCode(method.getParameterTypes()); CodeBlock injectionCode = CodeBlock.of("args -> $L.$L($L)", @@ -1083,7 +1084,6 @@ private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescr } } } - } } 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 fd41a5cfe0a1..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 @@ -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,11 +16,14 @@ package org.springframework.beans.factory.annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.LinkedHashMap; 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; @@ -33,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; /** @@ -49,7 +51,7 @@ public abstract class BeanFactoryAnnotationUtils { /** * Retrieve all beans of type {@code T} from the given {@code BeanFactory} declaring a - * qualifier (e.g. via {@code } or {@code @Qualifier}) matching the given + * qualifier (for example, via {@code } or {@code @Qualifier}) matching the given * qualifier, or having a bean name matching the given qualifier. * @param beanFactory the factory to get the target beans from (also searching ancestors) * @param beanType the type of beans to retrieve @@ -74,7 +76,7 @@ public static Map qualifiedBeansOfType( /** * Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a - * qualifier (e.g. via {@code } or {@code @Qualifier}) matching the given + * qualifier (for example, via {@code } or {@code @Qualifier}) matching the given * qualifier, or having a bean name matching the given qualifier. * @param beanFactory the factory to get the target bean from (also searching ancestors) * @param beanType the type of bean to retrieve @@ -94,7 +96,7 @@ public static T qualifiedBeanOfType(BeanFactory beanFactory, Class beanTy // Full qualifier matching supported. return qualifiedBeanOfType(lbf, beanType, qualifier); } - else if (beanFactory.containsBean(qualifier)) { + else if (beanFactory.containsBean(qualifier) && beanFactory.isTypeMatch(qualifier, beanType)) { // Fallback: target bean at least found by bean name. return beanFactory.getBean(qualifier, beanType); } @@ -108,17 +110,17 @@ else if (beanFactory.containsBean(qualifier)) { /** * Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a qualifier - * (e.g. {@code } or {@code @Qualifier}) matching the given qualifier). - * @param bf the factory to get the target bean from + * (for example, {@code } or {@code @Qualifier}) matching the given qualifier). + * @param beanFactory the factory to get the target bean from * @param beanType the type of bean to retrieve * @param qualifier the qualifier for selecting between multiple bean matches * @return the matching bean of type {@code T} (never {@code null}) */ - private static T qualifiedBeanOfType(ListableBeanFactory bf, Class beanType, String qualifier) { - String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(bf, beanType); + private static T qualifiedBeanOfType(ListableBeanFactory beanFactory, Class beanType, String qualifier) { + String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, beanType); String matchingBean = null; for (String beanName : candidateBeans) { - if (isQualifierMatch(qualifier::equals, beanName, bf)) { + if (isQualifierMatch(qualifier::equals, beanName, beanFactory)) { if (matchingBean != null) { throw new NoUniqueBeanDefinitionException(beanType, matchingBean, beanName); } @@ -126,11 +128,11 @@ private static T qualifiedBeanOfType(ListableBeanFactory bf, Class beanTy } } if (matchingBean != null) { - return bf.getBean(matchingBean, beanType); + return beanFactory.getBean(matchingBean, beanType); } - else if (bf.containsBean(qualifier)) { + else if (beanFactory.containsBean(qualifier) && beanFactory.isTypeMatch(qualifier, beanType)) { // Fallback: target bean at least found by bean name - probably a manually registered singleton. - return bf.getBean(qualifier, beanType); + return beanFactory.getBean(qualifier, beanType); } else { throw new NoSuchBeanDefinitionException(qualifier, "No matching " + beanType.getSimpleName() + @@ -138,6 +140,18 @@ else if (bf.containsBean(qualifier)) { } } + /** + * Determine the {@link Qualifier#value() qualifier value} for the given + * annotated element. + * @param annotatedElement the class, method or parameter to introspect + * @return the associated qualifier value, or {@code null} if none + * @since 6.2 + */ + public static @Nullable String getQualifierValue(AnnotatedElement annotatedElement) { + Qualifier qualifier = AnnotationUtils.getAnnotation(annotatedElement, Qualifier.class); + return (qualifier != null ? qualifier.value() : null); + } + /** * Check whether the named bean declares a qualifier of the given name. * @param qualifier the qualifier to match 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 708064488acf..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 @@ -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. @@ -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) { @@ -363,7 +359,7 @@ public LifecycleMetadata(Class beanClass, Collection initMet } public void checkInitDestroyMethods(RootBeanDefinition beanDefinition) { - Set checkedInitMethods = new LinkedHashSet<>(this.initMethods.size()); + Set checkedInitMethods = CollectionUtils.newLinkedHashSet(this.initMethods.size()); for (LifecycleMethod lifecycleMethod : this.initMethods) { String methodIdentifier = lifecycleMethod.getIdentifier(); if (!beanDefinition.isExternallyManagedInitMethod(methodIdentifier)) { @@ -374,7 +370,7 @@ public void checkInitDestroyMethods(RootBeanDefinition beanDefinition) { } } } - Set checkedDestroyMethods = new LinkedHashSet<>(this.destroyMethods.size()); + Set checkedDestroyMethods = CollectionUtils.newLinkedHashSet(this.destroyMethods.size()); for (LifecycleMethod lifecycleMethod : this.destroyMethods) { String methodIdentifier = lifecycleMethod.getIdentifier(); if (!beanDefinition.isExternallyManagedDestroyMethod(methodIdentifier)) { 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 d1a5946aa0fb..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 @@ -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,15 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; 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.Nullable; +import org.springframework.lang.Contract; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -71,8 +73,7 @@ public void clear(@Nullable PropertyValues pvs) { private final Collection injectedElements; - @Nullable - private volatile Set checkedElements; + private volatile @Nullable Set checkedElements; /** @@ -124,7 +125,7 @@ public void checkConfigMembers(RootBeanDefinition beanDefinition) { this.checkedElements = Collections.emptySet(); } else { - Set checkedElements = new LinkedHashSet<>((this.injectedElements.size() * 4 / 3) + 1); + Set checkedElements = CollectionUtils.newLinkedHashSet(this.injectedElements.size()); for (InjectedElement element : this.injectedElements) { Member member = element.getMember(); if (!beanDefinition.isExternallyManagedConfigMember(member)) { @@ -182,6 +183,7 @@ public static InjectionMetadata forElements(Collection elements * @return {@code true} indicating a refresh, {@code false} otherwise * @see #needsRefresh(Class) */ + @Contract("null, _ -> true") public static boolean needsRefresh(@Nullable InjectionMetadata metadata, Class clazz) { return (metadata == null || metadata.needsRefresh(clazz)); } @@ -196,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; @@ -333,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 c4e46f8ecd8a..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 @@ -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,22 +18,27 @@ 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. + * {@link RuntimeHintsRegistrar} for Jakarta annotations and their pre-Jakarta equivalents. * * @author Brian Clozel + * @author Sam Brannen */ class JakartaAnnotationsRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - Stream.of("jakarta.inject.Inject", "jakarta.inject.Provider", "jakarta.inject.Qualifier").forEach(typeName -> - hints.reflection().registerType(TypeReference.of(typeName))); + Stream.of( + "jakarta.inject.Inject", + "jakarta.inject.Provider", + "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 f8f7b0dce6f4..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 { @@ -153,7 +152,7 @@ public static Object resolveDependency( * an empty {@code AnnotatedElement}. *

WARNING

*

The {@code AnnotatedElement} returned by this method should never be cast and - * treated as a {@code Parameter} since the metadata (e.g., {@link Parameter#getName()}, + * treated as a {@code Parameter} since the metadata (for example, {@link Parameter#getName()}, * {@link Parameter#getType()}, etc.) will not match those for the declared parameter * at the given index in an inner class constructor. * @return the supplied {@code parameter} or the effective {@code Parameter} 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 cc926c37e5ca..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-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.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.util.LinkedHashSet; 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; @@ -37,9 +38,9 @@ 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; import org.springframework.util.ObjectUtils; /** @@ -47,11 +48,12 @@ * 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, if available. + *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation if available. * * @author Mark Fisher * @author Juergen Hoeller * @author Stephane Nicoll + * @author Sam Brannen * @since 2.5 * @see AutowireCandidateQualifier * @see Qualifier @@ -59,15 +61,15 @@ */ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwareAutowireCandidateResolver { - private final Set> qualifierTypes = new LinkedHashSet<>(2); + private final Set> qualifierTypes = CollectionUtils.newLinkedHashSet(2); private Class valueAnnotationType = Value.class; /** - * Create a new QualifierAnnotationAutowireCandidateResolver - * for Spring's standard {@link Qualifier} annotation. - *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation, if available. + * Create a new {@code QualifierAnnotationAutowireCandidateResolver} for Spring's + * standard {@link Qualifier} annotation. + *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation if available. */ @SuppressWarnings("unchecked") public QualifierAnnotationAutowireCandidateResolver() { @@ -77,13 +79,13 @@ public QualifierAnnotationAutowireCandidateResolver() { QualifierAnnotationAutowireCandidateResolver.class.getClassLoader())); } catch (ClassNotFoundException ex) { - // JSR-330 API not available - simply skip. + // JSR-330 API (as included in Jakarta EE) not available - simply skip. } } /** - * Create a new QualifierAnnotationAutowireCandidateResolver - * for the given qualifier annotation type. + * Create a new {@code QualifierAnnotationAutowireCandidateResolver} for the given + * qualifier annotation type. * @param qualifierType the qualifier annotation to look for */ public QualifierAnnotationAutowireCandidateResolver(Class qualifierType) { @@ -92,8 +94,8 @@ public QualifierAnnotationAutowireCandidateResolver(Class } /** - * Create a new QualifierAnnotationAutowireCandidateResolver - * for the given qualifier annotation types. + * Create a new {@code QualifierAnnotationAutowireCandidateResolver} for the given + * qualifier annotation types. * @param qualifierTypes the qualifier annotations to look for */ public QualifierAnnotationAutowireCandidateResolver(Set> qualifierTypes) { @@ -144,66 +146,92 @@ public void setValueAnnotationType(Class valueAnnotationTy */ @Override public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) { - boolean match = super.isAutowireCandidate(bdHolder, descriptor); - if (match) { - match = checkQualifiers(bdHolder, descriptor.getAnnotations()); - if (match) { - MethodParameter methodParam = descriptor.getMethodParameter(); - if (methodParam != null) { - Method method = methodParam.getMethod(); - if (method == null || void.class == method.getReturnType()) { - match = checkQualifiers(bdHolder, methodParam.getMethodAnnotations()); + if (!super.isAutowireCandidate(bdHolder, descriptor)) { + return false; + } + Boolean checked = checkQualifiers(bdHolder, descriptor.getAnnotations()); + if (checked != Boolean.FALSE) { + MethodParameter methodParam = descriptor.getMethodParameter(); + if (methodParam != null) { + Method method = methodParam.getMethod(); + if (method == null || void.class == method.getReturnType()) { + Boolean methodChecked = checkQualifiers(bdHolder, methodParam.getMethodAnnotations()); + if (methodChecked != null && checked == null) { + checked = methodChecked; } } } } - return match; + return (checked == Boolean.TRUE || + (checked == null && ((RootBeanDefinition) bdHolder.getBeanDefinition()).isDefaultCandidate())); } /** * Match the given qualifier annotations against the candidate bean definition. + * @return {@code false} if a qualifier has been found but not matched, + * {@code true} if a qualifier has been found and matched, + * {@code null} if no qualifier has been found at all */ - protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) { - if (ObjectUtils.isEmpty(annotationsToSearch)) { - return true; - } - SimpleTypeConverter typeConverter = new SimpleTypeConverter(); - for (Annotation annotation : annotationsToSearch) { - Class type = annotation.annotationType(); - boolean checkMeta = true; - boolean fallbackToMeta = false; - if (isQualifier(type)) { - if (!checkQualifier(bdHolder, annotation, typeConverter)) { - fallbackToMeta = true; + + protected @Nullable Boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) { + boolean qualifierFound = false; + if (!ObjectUtils.isEmpty(annotationsToSearch)) { + SimpleTypeConverter typeConverter = new SimpleTypeConverter(); + for (Annotation annotation : annotationsToSearch) { + Class type = annotation.annotationType(); + if (isPlainJavaAnnotation(type)) { + continue; } - else { - checkMeta = false; + boolean checkMeta = true; + boolean fallbackToMeta = false; + if (isQualifier(type)) { + qualifierFound = true; + if (!checkQualifier(bdHolder, annotation, typeConverter)) { + fallbackToMeta = true; + } + else { + checkMeta = false; + } } - } - if (checkMeta) { - boolean foundMeta = false; - for (Annotation metaAnn : type.getAnnotations()) { - Class metaType = metaAnn.annotationType(); - if (isQualifier(metaType)) { - foundMeta = true; - // Only accept fallback match if @Qualifier annotation has a value... - // Otherwise, it is just a marker for a custom qualifier annotation. - if ((fallbackToMeta && ObjectUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) || - !checkQualifier(bdHolder, metaAnn, typeConverter)) { - return false; + if (checkMeta) { + boolean foundMeta = false; + for (Annotation metaAnn : type.getAnnotations()) { + Class metaType = metaAnn.annotationType(); + if (isPlainJavaAnnotation(metaType)) { + continue; + } + if (isQualifier(metaType)) { + qualifierFound = true; + foundMeta = true; + // Only accept fallback match if @Qualifier annotation has a value... + // Otherwise, it is just a marker for a custom qualifier annotation. + if ((fallbackToMeta && ObjectUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) || + !checkQualifier(bdHolder, metaAnn, typeConverter)) { + return false; + } } } - } - if (fallbackToMeta && !foundMeta) { - return false; + if (fallbackToMeta && !foundMeta) { + return false; + } } } } - return true; + return (qualifierFound ? true : null); + } + + /** + * Check whether the given annotation type is a plain "java." annotation, + * typically from {@code java.lang.annotation}. + *

Aligned with + * {@code org.springframework.core.annotation.AnnotationsScanner#hasPlainJavaAnnotationsOnly}. + */ + private boolean isPlainJavaAnnotation(Class annotationType) { + return annotationType.getName().startsWith("java."); } /** - * Checks whether the given annotation type is a recognized qualifier type. + * Check whether the given annotation type is a recognized qualifier type. */ protected boolean isQualifier(Class annotationType) { for (Class qualifierType : this.qualifierTypes) { @@ -263,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; @@ -282,7 +310,7 @@ protected boolean checkQualifier( } if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) && expectedValue instanceof String name && bdHolder.matchesName(name)) { - // Fall back on bean name (or alias) match + // Finally, check bean name (or alias) match continue; } if (actualValue == null && qualifier != null) { @@ -292,21 +320,19 @@ protected boolean checkQualifier( if (actualValue != null) { actualValue = typeConverter.convertIfNecessary(actualValue, expectedValue.getClass()); } - if (!expectedValue.equals(actualValue)) { + if (!ObjectUtils.nullSafeEquals(expectedValue, actualValue)) { return false; } } 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); } @@ -333,21 +359,33 @@ public boolean isRequired(DependencyDescriptor descriptor) { */ @Override public boolean hasQualifier(DependencyDescriptor descriptor) { - for (Annotation ann : descriptor.getAnnotations()) { - if (isQualifier(ann.annotationType())) { + for (Annotation annotation : descriptor.getAnnotations()) { + if (isQualifier(annotation.annotationType())) { return true; } } return false; } + @Override + public @Nullable String getSuggestedName(DependencyDescriptor descriptor) { + for (Annotation annotation : descriptor.getAnnotations()) { + if (isQualifier(annotation.annotationType())) { + Object value = AnnotationUtils.getValue(annotation); + if (value instanceof String str) { + return str; + } + } + } + return null; + } + /** * Determine whether the given dependency declares a value annotation. * @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(); @@ -361,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 new file mode 100644 index 000000000000..69d4f79325d4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java @@ -0,0 +1,75 @@ +/* + * 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.beans.factory.aot; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; + +/** + * Thrown when AOT fails to process a bean. + * + * @author Stephane Nicoll + * @since 6.2 + */ +@SuppressWarnings("serial") +public class AotBeanProcessingException extends AotProcessingException { + + private final RootBeanDefinition beanDefinition; + + /** + * Create an instance with the {@link RegisteredBean} that fails to be + * processed, a detail message, and an optional root cause. + * @param registeredBean the registered bean that fails to be processed + * @param msg the detail message + * @param cause the root cause, if any + */ + public AotBeanProcessingException(RegisteredBean registeredBean, String msg, @Nullable Throwable cause) { + super(createErrorMessage(registeredBean, msg), cause); + this.beanDefinition = registeredBean.getMergedBeanDefinition(); + } + + /** + * Shortcut to create an instance with the {@link RegisteredBean} that fails + * to be processed with only a detail message. + * @param registeredBean the registered bean that fails to be processed + * @param msg the detail message + */ + public AotBeanProcessingException(RegisteredBean registeredBean, String msg) { + this(registeredBean, msg, null); + } + + private static String createErrorMessage(RegisteredBean registeredBean, String msg) { + StringBuilder sb = new StringBuilder("Error processing bean with name '"); + sb.append(registeredBean.getBeanName()).append("'"); + String resourceDescription = registeredBean.getMergedBeanDefinition().getResourceDescription(); + if (resourceDescription != null) { + sb.append(" defined in ").append(resourceDescription); + } + sb.append(": ").append(msg); + return sb.toString(); + } + + /** + * Return the bean definition of the bean that failed to be processed. + */ + public RootBeanDefinition getBeanDefinition() { + return this.beanDefinition; + } + +} 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 new file mode 100644 index 000000000000..2bbf63c60b3c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java @@ -0,0 +1,39 @@ +/* + * 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.beans.factory.aot; + +import org.jspecify.annotations.Nullable; + +/** + * Abstract superclass for all exceptions thrown by ahead-of-time processing. + * + * @author Stephane Nicoll + * @since 6.2 + */ +@SuppressWarnings("serial") +public abstract class AotException extends RuntimeException { + + /** + * Create an instance with the specified message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + protected AotException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} 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 new file mode 100644 index 000000000000..63762b02682b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java @@ -0,0 +1,39 @@ +/* + * 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.beans.factory.aot; + +import org.jspecify.annotations.Nullable; + +/** + * Throw when an AOT processor failed. + * + * @author Stephane Nicoll + * @since 6.2 + */ +@SuppressWarnings("serial") +public class AotProcessingException extends AotException { + + /** + * Create a new instance with the detail message and a root cause, if any. + * @param msg the detail message + * @param cause the root cause, if any + */ + public AotProcessingException(String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} 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 8a8f152cd7ca..0afc2419a808 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-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. @@ -60,21 +60,18 @@ public CodeBlock generateCode(Class[] parameterTypes, int startIndex) { return generateCode(parameterTypes, startIndex, "args"); } - public CodeBlock generateCode(Class[] parameterTypes, int startIndex, - String variableName) { - + public CodeBlock generateCode(Class[] parameterTypes, int startIndex, String variableName) { Assert.notNull(parameterTypes, "'parameterTypes' must not be null"); Assert.notNull(variableName, "'variableName' must not be null"); boolean ambiguous = isAmbiguous(); CodeBlock.Builder code = CodeBlock.builder(); for (int i = startIndex; i < parameterTypes.length; i++) { - code.add((i != startIndex) ? ", " : ""); + code.add(i > startIndex ? ", " : ""); if (!ambiguous) { code.add("$L.get($L)", variableName, i - startIndex); } else { - code.add("$L.get($L, $T.class)", variableName, i - startIndex, - parameterTypes[i]); + code.add("$L.get($L, $T.class)", variableName, i - startIndex, 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 12cfc76b0833..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 @@ -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,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,17 +58,14 @@ public final class AutowiredFieldValueResolver extends AutowiredElementResolver private final boolean required; - @Nullable - private final String shortcut; - + private final @Nullable String shortcutBeanName; - private AutowiredFieldValueResolver(String fieldName, boolean required, - @Nullable String shortcut) { + private AutowiredFieldValueResolver(String fieldName, boolean required, @Nullable String shortcut) { Assert.hasText(fieldName, "'fieldName' must not be empty"); this.fieldName = fieldName; this.required = required; - this.shortcut = shortcut; + this.shortcutBeanName = shortcut; } @@ -97,7 +95,7 @@ public static AutowiredFieldValueResolver forRequiredField(String fieldName) { * direct bean name injection shortcut. * @param beanName the bean name to use as a shortcut * @return a new {@link AutowiredFieldValueResolver} instance that uses the - * shortcuts + * given shortcut bean name */ public AutowiredFieldValueResolver withShortcut(String beanName) { return new AutowiredFieldValueResolver(this.fieldName, this.required, beanName); @@ -124,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; @@ -137,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); } @@ -148,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)); } @@ -171,15 +166,14 @@ 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(); DependencyDescriptor descriptor = new DependencyDescriptor(field, this.required); descriptor.setContainingClass(beanClass); - if (this.shortcut != null) { - descriptor = new ShortcutDependencyDescriptor(descriptor, this.shortcut); + if (this.shortcutBeanName != null) { + descriptor = new ShortcutDependencyDescriptor(descriptor, this.shortcutBeanName); } Set autowiredBeanNames = new LinkedHashSet<>(1); TypeConverter typeConverter = beanFactory.getTypeConverter(); @@ -191,16 +185,14 @@ private Object resolveValue(RegisteredBean registeredBean, Field field) { return value; } catch (BeansException ex) { - throw new UnsatisfiedDependencyException(null, beanName, - new InjectionPoint(field), ex); + throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex); } } private Field getField(RegisteredBean registeredBean) { - Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), - this.fieldName); - Assert.notNull(field, () -> "No field '" + this.fieldName + "' found on " - + registeredBean.getBeanClass().getName()); + Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), this.fieldName); + Assert.notNull(field, () -> "No field '" + this.fieldName + "' found on " + + registeredBean.getBeanClass().getName()); return field; } 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 9c79f232114a..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-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,10 +18,11 @@ import java.lang.reflect.Method; import java.util.Arrays; -import java.util.LinkedHashSet; 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; @@ -32,8 +33,8 @@ 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; import org.springframework.util.function.ThrowingConsumer; @@ -62,18 +63,17 @@ public final class AutowiredMethodArgumentsResolver extends AutowiredElementReso private final boolean required; - @Nullable - private final String[] shortcuts; + private final String @Nullable [] shortcutBeanNames; private AutowiredMethodArgumentsResolver(String methodName, Class[] parameterTypes, - boolean required, @Nullable String[] shortcuts) { + boolean required, String @Nullable [] shortcutBeanNames) { Assert.hasText(methodName, "'methodName' must not be empty"); this.methodName = methodName; this.parameterTypes = parameterTypes; this.required = required; - this.shortcuts = shortcuts; + this.shortcutBeanNames = shortcutBeanNames; } @@ -105,7 +105,7 @@ public static AutowiredMethodArgumentsResolver forRequiredMethod(String methodNa * @param beanNames the bean names to use as shortcuts (aligned with the * method parameters) * @return a new {@link AutowiredMethodArgumentsResolver} instance that uses - * the shortcuts + * the given shortcut bean names */ public AutowiredMethodArgumentsResolver withShortcut(String... beanNames) { return new AutowiredMethodArgumentsResolver(this.methodName, this.parameterTypes, this.required, beanNames); @@ -131,8 +131,7 @@ public void resolve(RegisteredBean registeredBean, ThrowingConsumer autowiredBeanNames = new LinkedHashSet<>(argumentCount); + @Nullable Object[] arguments = new Object[argumentCount]; + Set autowiredBeanNames = CollectionUtils.newLinkedHashSet(argumentCount); TypeConverter typeConverter = beanFactory.getTypeConverter(); for (int i = 0; i < argumentCount; i++) { MethodParameter parameter = new MethodParameter(method, i); DependencyDescriptor descriptor = new DependencyDescriptor(parameter, this.required); descriptor.setContainingClass(beanClass); - String shortcut = (this.shortcuts != null ? this.shortcuts[i] : null); + String shortcut = (this.shortcutBeanNames != null ? this.shortcutBeanNames[i] : null); if (shortcut != null) { descriptor = new ShortcutDependencyDescriptor(descriptor, shortcut); } 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 01c2ab71a7ca..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; @@ -165,7 +165,7 @@ private GeneratedMethod generateBeanDefinitionMethod(GenerationContext generatio this.aotContributions.forEach(aotContribution -> aotContribution.applyTo(generationContext, codeGenerator)); CodeWarnings codeWarnings = new CodeWarnings(); - codeWarnings.detectDeprecation(this.registeredBean.getBeanClass()); + codeWarnings.detectDeprecation(this.registeredBean.getBeanType()); return generatedMethods.add("getBeanDefinition", method -> { method.addJavadoc("Get the $L definition for '$L'.", (this.registeredBean.isInnerBean() ? "inner-bean" : "bean"), 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 580a0533cedb..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); } @@ -133,6 +131,10 @@ private boolean isExcluded(RegisteredBean registeredBean) { } private boolean isImplicitlyExcluded(RegisteredBean registeredBean) { + if (Boolean.TRUE.equals(registeredBean.getMergedBeanDefinition() + .getAttribute(BeanRegistrationAotProcessor.IGNORE_REGISTRATION_ATTRIBUTE))) { + return true; + } Class beanClass = registeredBean.getBeanClass(); if (BeanFactoryInitializationAotProcessor.class.isAssignableFrom(beanClass)) { return true; 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 a0133f88faf5..6aca2880b71d 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-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,12 @@ 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; @@ -52,7 +57,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; @@ -89,40 +93,55 @@ class BeanDefinitionPropertiesCodeGenerator { private final Predicate attributeFilter; - private final BeanDefinitionPropertyValueCodeGenerator valueCodeGenerator; + private final ValueCodeGenerator valueCodeGenerator; BeanDefinitionPropertiesCodeGenerator(RuntimeHints hints, Predicate attributeFilter, GeneratedMethods generatedMethods, + List additionalDelegates, BiFunction customValueCodeGenerator) { this.hints = hints; this.attributeFilter = attributeFilter; - this.valueCodeGenerator = new BeanDefinitionPropertyValueCodeGenerator(generatedMethods, - (object, type) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), object)); + 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); } - + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1128 CodeBlock generateCode(RootBeanDefinition beanDefinition) { CodeBlock.Builder code = CodeBlock.builder(); - addStatementForValue(code, beanDefinition, BeanDefinition::isPrimary, - "$L.setPrimary($L)"); addStatementForValue(code, beanDefinition, BeanDefinition::getScope, this::hasScope, "$L.setScope($S)"); + addStatementForValue(code, beanDefinition, AbstractBeanDefinition::isBackgroundInit, + "$L.setBackgroundInit($L)"); + addStatementForValue(code, beanDefinition, AbstractBeanDefinition::getLazyInit, + "$L.setLazyInit($L)"); addStatementForValue(code, beanDefinition, BeanDefinition::getDependsOn, this::hasDependsOn, "$L.setDependsOn($L)", this::toStringVarArgs); addStatementForValue(code, beanDefinition, BeanDefinition::isAutowireCandidate, "$L.setAutowireCandidate($L)"); - addStatementForValue(code, beanDefinition, BeanDefinition::getRole, - this::hasRole, "$L.setRole($L)", this::toRole); - addStatementForValue(code, beanDefinition, AbstractBeanDefinition::getLazyInit, - "$L.setLazyInit($L)"); + addStatementForValue(code, beanDefinition, AbstractBeanDefinition::isDefaultCandidate, + "$L.setDefaultCandidate($L)"); + addStatementForValue(code, beanDefinition, BeanDefinition::isPrimary, + "$L.setPrimary($L)"); + addStatementForValue(code, beanDefinition, BeanDefinition::isFallback, + "$L.setFallback($L)"); addStatementForValue(code, beanDefinition, AbstractBeanDefinition::isSynthetic, "$L.setSynthetic($L)"); + addStatementForValue(code, beanDefinition, BeanDefinition::getRole, + this::hasRole, "$L.setRole($L)", this::toRole); addInitDestroyMethods(code, beanDefinition, beanDefinition.getInitMethodNames(), "$L.setInitMethodNames($L)"); addInitDestroyMethods(code, beanDefinition, beanDefinition.getDestroyMethodNames(), "$L.setDestroyMethodNames($L)"); + if (beanDefinition.getFactoryBeanName() != null) { + addStatementForValue(code, beanDefinition, BeanDefinition::getFactoryBeanName, + "$L.setFactoryBeanName(\"$L\")"); + } addConstructorArgumentValues(code, beanDefinition); addPropertyValues(code, beanDefinition); addAttributes(code, beanDefinition); @@ -131,7 +150,8 @@ 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")); if (!ObjectUtils.isEmpty(methodNames)) { @@ -166,6 +186,10 @@ private void addInitDestroyHint(Class beanUserClass, String methodName) { Method method = ReflectionUtils.findMethod(methodDeclaringClass, methodName); if (method != null) { this.hints.reflection().registerMethod(method, ExecutableMode.INVOKE); + Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(method, beanUserClass); + if (!publiclyAccessibleMethod.equals(method)) { + this.hints.reflection().registerMethod(publiclyAccessibleMethod, ExecutableMode.INVOKE); + } } } @@ -196,7 +220,6 @@ private void addConstructorArgumentValues(CodeBlock.Builder code, BeanDefinition else if (valueHolder.getType() != null) { code.addStatement("$L.getConstructorArgumentValues().addGenericArgumentValue($L, $S)", BEAN_DEFINITION_VARIABLE, valueCode, valueHolder.getType()); - } else { code.addStatement("$L.getConstructorArgumentValues().addGenericArgumentValue($L)", @@ -209,32 +232,33 @@ else if (valueHolder.getType() != null) { private void addPropertyValues(CodeBlock.Builder code, RootBeanDefinition beanDefinition) { MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); if (!propertyValues.isEmpty()) { + Class infrastructureType = getInfrastructureType(beanDefinition); + Map writeMethods = (infrastructureType != Object.class ? + getWriteMethods(infrastructureType) : Collections.emptyMap()); for (PropertyValue propertyValue : propertyValues) { String name = propertyValue.getName(); CodeBlock valueCode = generateValue(name, propertyValue.getValue()); code.addStatement("$L.getPropertyValues().addPropertyValue($S, $L)", - BEAN_DEFINITION_VARIABLE, propertyValue.getName(), valueCode); - } - Class infrastructureType = getInfrastructureType(beanDefinition); - if (infrastructureType != Object.class) { - Map writeMethods = getWriteMethods(infrastructureType); - for (PropertyValue propertyValue : propertyValues) { - Method writeMethod = writeMethods.get(propertyValue.getName()); - if (writeMethod != null) { - this.hints.reflection().registerMethod(writeMethod, ExecutableMode.INVOKE); - // 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); - searchType = searchType.getSuperclass(); - } - this.hints.reflection().registerType(writeMethod.getDeclaringClass(), MemberCategory.DECLARED_FIELDS); - } + BEAN_DEFINITION_VARIABLE, name, valueCode); + Method writeMethod = writeMethods.get(name); + if (writeMethod != null) { + registerReflectionHints(beanDefinition, writeMethod); } } } } + private void registerReflectionHints(RootBeanDefinition beanDefinition, Method writeMethod) { + this.hints.reflection().registerMethod(writeMethod, ExecutableMode.INVOKE); + // ReflectionUtils#findField searches recursively in the type hierarchy + Class searchType = beanDefinition.getTargetType(); + while (searchType != null && searchType != writeMethod.getDeclaringClass()) { + this.hints.reflection().registerType(searchType, MemberCategory.ACCESS_DECLARED_FIELDS); + searchType = searchType.getSuperclass(); + } + this.hints.reflection().registerType(writeMethod.getDeclaringClass(), MemberCategory.ACCESS_DECLARED_FIELDS); + } + private void addQualifiers(CodeBlock.Builder code, RootBeanDefinition beanDefinition) { Set qualifiers = beanDefinition.getQualifiers(); if (!qualifiers.isEmpty()) { @@ -252,8 +276,8 @@ private void addQualifiers(CodeBlock.Builder code, RootBeanDefinition beanDefini } private CodeBlock generateValue(@Nullable String name, @Nullable Object value) { + PropertyNamesStack.push(name); try { - PropertyNamesStack.push(name); return this.valueCodeGenerator.generateCode(value); } finally { @@ -294,8 +318,7 @@ private void addAttributes(CodeBlock.Builder code, BeanDefinition beanDefinition } private boolean hasScope(String defaultValue, String actualValue) { - return StringUtils.hasText(actualValue) && - !ConfigurableBeanFactory.SCOPE_SINGLETON.equals(actualValue); + return (StringUtils.hasText(actualValue) && !ConfigurableBeanFactory.SCOPE_SINGLETON.equals(actualValue)); } private boolean hasDependsOn(String[] defaultValue, String[] actualValue) { @@ -321,8 +344,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); @@ -330,16 +352,15 @@ 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, BiPredicate filter, String format, - Function formatter) { + CodeBlock.Builder code, BeanDefinition beanDefinition, Function getter, + BiPredicate filter, String format, Function formatter) { T defaultValue = getter.apply((B) DEFAULT_BEAN_DEFINITION); T actualValue = getter.apply((B) beanDefinition); @@ -349,19 +370,19 @@ private void addStatementForValue( } /** - * Cast the specified {@code valueCode} to the specified {@code castType} if - * the {@code castNecessary} is {@code true}. Otherwise return the valueCode - * as is. + * Cast the specified {@code valueCode} to the specified {@code castType} if the + * {@code castNecessary} is {@code true}. Otherwise, return the valueCode as-is. * @param castNecessary whether a cast is necessary * @param castType the type to cast to * @param valueCode the code for the value - * @return the existing value or a form of {@code (CastType) valueCode} if a + * @return the existing value or a form of {@code (castType) valueCode} if a * cast is necessary */ private CodeBlock castIfNecessary(boolean castNecessary, Class castType, CodeBlock valueCode) { return (castNecessary ? CodeBlock.of("($T) $L", castType, valueCode) : valueCode); } + static class PropertyNamesStack { private static final ThreadLocal> threadLocal = ThreadLocal.withInitial(ArrayDeque::new); @@ -375,12 +396,10 @@ 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/BeanDefinitionPropertyValueCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java deleted file mode 100644 index 066fc5d4b1e6..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGenerator.java +++ /dev/null @@ -1,600 +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.beans.factory.aot; - -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.function.BiFunction; -import java.util.stream.Stream; - -import org.springframework.aot.generate.GeneratedMethod; -import org.springframework.aot.generate.GeneratedMethods; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanReference; -import org.springframework.beans.factory.config.RuntimeBeanReference; -import org.springframework.beans.factory.config.TypedStringValue; -import org.springframework.beans.factory.support.ManagedList; -import org.springframework.beans.factory.support.ManagedMap; -import org.springframework.beans.factory.support.ManagedSet; -import org.springframework.core.ResolvableType; -import org.springframework.javapoet.AnnotationSpec; -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; - -/** - * Internal code generator used to generate code for a single value contained in - * a {@link BeanDefinition} property. - * - * @author Stephane Nicoll - * @author Phillip Webb - * @author Sebastien Deleuze - * @since 6.0 - */ -class BeanDefinitionPropertyValueCodeGenerator { - - static final CodeBlock NULL_VALUE_CODE_BLOCK = CodeBlock.of("null"); - - private final GeneratedMethods generatedMethods; - - private final List delegates; - - - BeanDefinitionPropertyValueCodeGenerator(GeneratedMethods generatedMethods, - @Nullable BiFunction customValueGenerator) { - - this.generatedMethods = generatedMethods; - this.delegates = new ArrayList<>(); - if (customValueGenerator != null) { - this.delegates.add(customValueGenerator::apply); - } - this.delegates.addAll(List.of( - new PrimitiveDelegate(), - new StringDelegate(), - new CharsetDelegate(), - new EnumDelegate(), - new ClassDelegate(), - new ResolvableTypeDelegate(), - new ArrayDelegate(), - new ManagedListDelegate(), - new ManagedSetDelegate(), - new ManagedMapDelegate(), - new ListDelegate(), - new SetDelegate(), - new MapDelegate(), - new BeanReferenceDelegate(), - new TypedStringValueDelegate() - )); - } - - - CodeBlock generateCode(@Nullable Object value) { - ResolvableType type = ResolvableType.forInstance(value); - try { - return generateCode(value, type); - } - catch (Exception ex) { - throw new IllegalArgumentException(buildErrorMessage(value, type), ex); - } - } - - private CodeBlock generateCodeForElement(@Nullable Object value, ResolvableType type) { - try { - return generateCode(value, type); - } - catch (Exception ex) { - throw new IllegalArgumentException(buildErrorMessage(value, type), ex); - } - } - - private static String buildErrorMessage(@Nullable Object value, ResolvableType type) { - StringBuilder message = new StringBuilder("Failed to generate code for '"); - message.append(value).append("'"); - if (type != ResolvableType.NONE) { - message.append(" with type ").append(type); - } - return message.toString(); - } - - private CodeBlock generateCode(@Nullable Object value, ResolvableType type) { - if (value == null) { - return NULL_VALUE_CODE_BLOCK; - } - for (Delegate delegate : this.delegates) { - CodeBlock code = delegate.generateCode(value, type); - if (code != null) { - return code; - } - } - throw new IllegalArgumentException("Code generation does not support " + type); - } - - - /** - * Internal delegate used to support generation for a specific type. - */ - @FunctionalInterface - private interface Delegate { - - @Nullable - CodeBlock generateCode(Object value, ResolvableType type); - } - - - /** - * {@link Delegate} for {@code primitive} types. - */ - private static class PrimitiveDelegate implements Delegate { - - private static final Map CHAR_ESCAPES = Map.of( - '\b', "\\b", - '\t', "\\t", - '\n', "\\n", - '\f', "\\f", - '\r', "\\r", - '\"', "\"", - '\'', "\\'", - '\\', "\\\\" - ); - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Boolean || value instanceof Integer) { - return CodeBlock.of("$L", value); - } - if (value instanceof Byte) { - return CodeBlock.of("(byte) $L", value); - } - if (value instanceof Short) { - return CodeBlock.of("(short) $L", value); - } - if (value instanceof Long) { - return CodeBlock.of("$LL", value); - } - if (value instanceof Float) { - return CodeBlock.of("$LF", value); - } - if (value instanceof Double) { - return CodeBlock.of("(double) $L", value); - } - if (value instanceof Character character) { - return CodeBlock.of("'$L'", escape(character)); - } - return null; - } - - private String escape(char ch) { - String escaped = CHAR_ESCAPES.get(ch); - if (escaped != null) { - return escaped; - } - return (!Character.isISOControl(ch)) ? Character.toString(ch) - : String.format("\\u%04x", (int) ch); - } - } - - - /** - * {@link Delegate} for {@link String} types. - */ - private static class StringDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof String) { - return CodeBlock.of("$S", value); - } - return null; - } - } - - - /** - * {@link Delegate} for {@link Charset} types. - */ - private static class CharsetDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Charset charset) { - return CodeBlock.of("$T.forName($S)", Charset.class, charset.name()); - } - return null; - } - } - - - /** - * {@link Delegate} for {@link Enum} types. - */ - private static class EnumDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Enum enumValue) { - return CodeBlock.of("$T.$L", enumValue.getDeclaringClass(), - enumValue.name()); - } - return null; - } - } - - - /** - * {@link Delegate} for {@link Class} types. - */ - private static class ClassDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Class clazz) { - return CodeBlock.of("$T.class", ClassUtils.getUserClass(clazz)); - } - return null; - } - } - - - /** - * {@link Delegate} for {@link ResolvableType} types. - */ - private static class ResolvableTypeDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof ResolvableType resolvableType) { - return ResolvableTypeCodeGenerator.generateCode(resolvableType); - } - return null; - } - } - - - /** - * {@link Delegate} for {@code array} types. - */ - private class ArrayDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(@Nullable Object value, ResolvableType type) { - if (type.isArray()) { - ResolvableType componentType = type.getComponentType(); - Stream elements = Arrays.stream(ObjectUtils.toObjectArray(value)).map(component -> - BeanDefinitionPropertyValueCodeGenerator.this.generateCode(component, componentType)); - CodeBlock.Builder code = CodeBlock.builder(); - code.add("new $T {", type.toClass()); - code.add(elements.collect(CodeBlock.joining(", "))); - code.add("}"); - return code.build(); - } - return null; - } - } - - - /** - * Abstract {@link Delegate} for {@code Collection} types. - */ - private abstract class CollectionDelegate> implements Delegate { - - private final Class collectionType; - - private final CodeBlock emptyResult; - - public CollectionDelegate(Class collectionType, CodeBlock emptyResult) { - this.collectionType = collectionType; - this.emptyResult = emptyResult; - } - - @SuppressWarnings("unchecked") - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (this.collectionType.isInstance(value)) { - T collection = (T) value; - if (collection.isEmpty()) { - return this.emptyResult; - } - ResolvableType elementType = type.as(this.collectionType).getGeneric(); - return generateCollectionCode(elementType, collection); - } - return null; - } - - protected CodeBlock generateCollectionCode(ResolvableType elementType, T collection) { - return generateCollectionOf(collection, this.collectionType, elementType); - } - - protected final CodeBlock generateCollectionOf(Collection collection, - Class collectionType, ResolvableType elementType) { - Builder code = CodeBlock.builder(); - code.add("$T.of(", collectionType); - Iterator iterator = collection.iterator(); - while (iterator.hasNext()) { - Object element = iterator.next(); - code.add("$L", BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(element, elementType)); - if (iterator.hasNext()) { - code.add(", "); - } - } - code.add(")"); - return code.build(); - } - } - - - /** - * {@link Delegate} for {@link ManagedList} types. - */ - private class ManagedListDelegate extends CollectionDelegate> { - - public ManagedListDelegate() { - super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class)); - } - } - - - /** - * {@link Delegate} for {@link ManagedSet} types. - */ - private class ManagedSetDelegate extends CollectionDelegate> { - - public ManagedSetDelegate() { - super(ManagedSet.class, CodeBlock.of("new $T()", ManagedSet.class)); - } - } - - - /** - * {@link Delegate} for {@link ManagedMap} types. - */ - private class ManagedMapDelegate implements Delegate { - - private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class); - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof ManagedMap managedMap) { - return generateManagedMapCode(type, managedMap); - } - return null; - } - - private CodeBlock generateManagedMapCode(ResolvableType type, ManagedMap managedMap) { - if (managedMap.isEmpty()) { - return EMPTY_RESULT; - } - ResolvableType keyType = type.as(Map.class).getGeneric(0); - ResolvableType valueType = type.as(Map.class).getGeneric(1); - CodeBlock.Builder code = CodeBlock.builder(); - code.add("$T.ofEntries(", ManagedMap.class); - Iterator> iterator = managedMap.entrySet().iterator(); - while (iterator.hasNext()) { - Entry entry = iterator.next(); - code.add("$T.entry($L,$L)", Map.class, - BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(entry.getKey(), keyType), - BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(entry.getValue(), valueType)); - if (iterator.hasNext()) { - code.add(", "); - } - } - code.add(")"); - return code.build(); - } - } - - - /** - * {@link Delegate} for {@link List} types. - */ - private class ListDelegate extends CollectionDelegate> { - - ListDelegate() { - super(List.class, CodeBlock.of("$T.emptyList()", Collections.class)); - } - } - - - /** - * {@link Delegate} for {@link Set} types. - */ - private class SetDelegate extends CollectionDelegate> { - - SetDelegate() { - super(Set.class, CodeBlock.of("$T.emptySet()", Collections.class)); - } - - @Override - protected CodeBlock generateCollectionCode(ResolvableType elementType, Set set) { - if (set instanceof LinkedHashSet) { - return CodeBlock.of("new $T($L)", LinkedHashSet.class, - generateCollectionOf(set, List.class, elementType)); - } - return super.generateCollectionCode(elementType, orderForCodeConsistency(set)); - } - - private Set orderForCodeConsistency(Set set) { - try { - return new TreeSet(set); - } - catch (ClassCastException ex) { - // If elements are not comparable, just keep the original set - return set; - } - } - } - - - /** - * {@link Delegate} for {@link Map} types. - */ - private class MapDelegate implements Delegate { - - private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.emptyMap()", Collections.class); - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof Map map) { - return generateMapCode(type, map); - } - return null; - } - - private CodeBlock generateMapCode(ResolvableType type, Map map) { - if (map.isEmpty()) { - return EMPTY_RESULT; - } - ResolvableType keyType = type.as(Map.class).getGeneric(0); - ResolvableType valueType = type.as(Map.class).getGeneric(1); - if (map instanceof LinkedHashMap) { - return generateLinkedHashMapCode(map, keyType, valueType); - } - map = orderForCodeConsistency(map); - boolean useOfEntries = map.size() > 10; - CodeBlock.Builder code = CodeBlock.builder(); - code.add("$T" + ((!useOfEntries) ? ".of(" : ".ofEntries("), Map.class); - Iterator> iterator = map.entrySet().iterator(); - while (iterator.hasNext()) { - Entry entry = iterator.next(); - CodeBlock keyCode = BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(entry.getKey(), keyType); - CodeBlock valueCode = BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(entry.getValue(), valueType); - if (!useOfEntries) { - code.add("$L, $L", keyCode, valueCode); - } - else { - code.add("$T.entry($L,$L)", Map.class, keyCode, valueCode); - } - if (iterator.hasNext()) { - code.add(", "); - } - } - code.add(")"); - return code.build(); - } - - private Map orderForCodeConsistency(Map map) { - try { - return new TreeMap<>(map); - } - catch (ClassCastException ex) { - // If elements are not comparable, just keep the original map - return map; - } - } - - private CodeBlock generateLinkedHashMapCode(Map map, - ResolvableType keyType, ResolvableType valueType) { - - GeneratedMethods generatedMethods = BeanDefinitionPropertyValueCodeGenerator.this.generatedMethods; - GeneratedMethod generatedMethod = generatedMethods.add("getMap", method -> { - method.addAnnotation(AnnotationSpec - .builder(SuppressWarnings.class) - .addMember("value", "{\"rawtypes\", \"unchecked\"}") - .build()); - method.returns(Map.class); - method.addStatement("$T map = new $T($L)", Map.class, - LinkedHashMap.class, map.size()); - map.forEach((key, value) -> method.addStatement("map.put($L, $L)", - BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(key, keyType), - BeanDefinitionPropertyValueCodeGenerator.this - .generateCodeForElement(value, valueType))); - method.addStatement("return map"); - }); - return CodeBlock.of("$L()", generatedMethod.getName()); - } - } - - - /** - * {@link Delegate} for {@link BeanReference} types. - */ - private static class BeanReferenceDelegate implements Delegate { - - @Override - @Nullable - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof RuntimeBeanReference runtimeBeanReference && - runtimeBeanReference.getBeanType() != null) { - return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class, - runtimeBeanReference.getBeanType()); - } - else if (value instanceof BeanReference beanReference) { - return CodeBlock.of("new $T($S)", RuntimeBeanReference.class, - beanReference.getBeanName()); - } - return null; - } - } - - - /** - * {@link Delegate} for {@link TypedStringValue} types. - */ - private class TypedStringValueDelegate implements Delegate { - - @Override - public CodeBlock generateCode(Object value, ResolvableType type) { - if (value instanceof TypedStringValue typedStringValue) { - return generateTypeStringValueCode(typedStringValue); - } - return null; - } - - private CodeBlock generateTypeStringValueCode(TypedStringValue typedStringValue) { - String value = typedStringValue.getValue(); - if (typedStringValue.hasTargetType()) { - return CodeBlock.of("new $T($S, $L)", TypedStringValue.class, value, - generateCode(typedStringValue.getTargetType())); - } - return generateCode(value); - } - - private CodeBlock generateCode(@Nullable Object value) { - return BeanDefinitionPropertyValueCodeGenerator.this.generateCode(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 new file mode 100644 index 000000000000..cfdce0adf5fa --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-2025 the original author 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.aot; + +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; +import org.springframework.aot.generate.ValueCodeGenerator.Delegate; +import org.springframework.aot.generate.ValueCodeGeneratorDelegates; +import org.springframework.aot.generate.ValueCodeGeneratorDelegates.CollectionDelegate; +import org.springframework.aot.generate.ValueCodeGeneratorDelegates.MapDelegate; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.javapoet.AnnotationSpec; +import org.springframework.javapoet.CodeBlock; + +/** + * Code generator {@link Delegate} for common bean definition property values. + * + * @author Stephane Nicoll + * @since 6.1.2 + */ +abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { + + /** + * A list of {@link Delegate} implementations for the following common bean + * definition property value types. + *
    + *
  • {@link ManagedList}
  • + *
  • {@link ManagedSet}
  • + *
  • {@link ManagedMap}
  • + *
  • {@link LinkedHashMap}
  • + *
  • {@link BeanReference}
  • + *
  • {@link TypedStringValue}
  • + *
+ * When combined with {@linkplain ValueCodeGeneratorDelegates#INSTANCES the + * delegates for common value types}, this should be added first as they have + * special handling for list, set, and map. + */ + public static final List INSTANCES = List.of( + new ManagedListDelegate(), + new ManagedSetDelegate(), + new ManagedMapDelegate(), + new LinkedHashMapDelegate(), + new BeanReferenceDelegate(), + new TypedStringValueDelegate() + ); + + + /** + * {@link Delegate} for {@link ManagedList} types. + */ + private static class ManagedListDelegate extends CollectionDelegate> { + + public ManagedListDelegate() { + super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class)); + } + } + + + /** + * {@link Delegate} for {@link ManagedSet} types. + */ + private static class ManagedSetDelegate extends CollectionDelegate> { + + public ManagedSetDelegate() { + super(ManagedSet.class, CodeBlock.of("new $T()", ManagedSet.class)); + } + } + + + /** + * {@link Delegate} for {@link ManagedMap} types. + */ + private static class ManagedMapDelegate implements Delegate { + + private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class); + + @Override + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof ManagedMap managedMap) { + return generateManagedMapCode(valueCodeGenerator, managedMap); + } + return null; + } + + private CodeBlock generateManagedMapCode(ValueCodeGenerator valueCodeGenerator, + ManagedMap managedMap) { + if (managedMap.isEmpty()) { + return EMPTY_RESULT; + } + CodeBlock.Builder code = CodeBlock.builder(); + code.add("$T.ofEntries(", ManagedMap.class); + Iterator> iterator = managedMap.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + code.add("$T.entry($L,$L)", Map.class, + valueCodeGenerator.generateCode(entry.getKey()), + valueCodeGenerator.generateCode(entry.getValue())); + if (iterator.hasNext()) { + code.add(", "); + } + } + code.add(")"); + return code.build(); + } + } + + + /** + * {@link Delegate} for {@link Map} types. + */ + private static class LinkedHashMapDelegate extends MapDelegate { + + @Override + protected @Nullable CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map map) { + GeneratedMethods generatedMethods = valueCodeGenerator.getGeneratedMethods(); + if (map instanceof LinkedHashMap && generatedMethods != null) { + return generateLinkedHashMapCode(valueCodeGenerator, generatedMethods, map); + } + return super.generateMapCode(valueCodeGenerator, map); + } + + private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerator, + GeneratedMethods generatedMethods, Map map) { + + GeneratedMethod generatedMethod = generatedMethods.add("getMap", method -> { + method.addAnnotation(AnnotationSpec + .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()); + map.forEach((key, value) -> method.addStatement("map.put($L, $L)", + valueCodeGenerator.generateCode(key), + valueCodeGenerator.generateCode(value))); + method.addStatement("return map"); + }); + return CodeBlock.of("$L()", generatedMethod.getName()); + } + } + + + /** + * {@link Delegate} for {@link BeanReference} types. + */ + private static class BeanReferenceDelegate implements Delegate { + + @Override + 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, + runtimeBeanReference.getBeanType()); + } + else if (value instanceof BeanReference beanReference) { + return CodeBlock.of("new $T($S)", RuntimeBeanReference.class, + beanReference.getBeanName()); + } + return null; + } + } + + + /** + * {@link Delegate} for {@link TypedStringValue} types. + */ + private static class TypedStringValueDelegate implements Delegate { + + @Override + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof TypedStringValue typedStringValue) { + return generateTypeStringValueCode(valueCodeGenerator, typedStringValue); + } + return null; + } + + private CodeBlock generateTypeStringValueCode(ValueCodeGenerator valueCodeGenerator, TypedStringValue typedStringValue) { + String value = typedStringValue.getValue(); + if (typedStringValue.hasTargetType()) { + return CodeBlock.of("new $T($S, $L)", TypedStringValue.class, value, + valueCodeGenerator.generateCode(typedStringValue.getTargetType())); + } + return valueCodeGenerator.generateCode(value); + } + } +} 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 56d3e79268c7..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-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,20 +22,19 @@ import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.util.Arrays; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; 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; import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.UnsatisfiedDependencyException; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.config.DependencyDescriptor; @@ -46,10 +45,9 @@ 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.ObjectUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.function.ThrowingBiFunction; import org.springframework.util.function.ThrowingFunction; @@ -79,6 +77,7 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @author Juergen Hoeller * @since 6.0 * @param the type of instance supplied by this supplier * @see AutowiredArguments @@ -87,20 +86,22 @@ public final class BeanInstanceSupplier extends AutowiredElementResolver impl private final ExecutableLookup lookup; - @Nullable - private final ThrowingBiFunction generator; + private final @Nullable ThrowingFunction generatorWithoutArguments; + + private final @Nullable ThrowingBiFunction generatorWithArguments; - @Nullable - private final String[] shortcuts; + private final String @Nullable [] shortcutBeanNames; private BeanInstanceSupplier(ExecutableLookup lookup, - @Nullable ThrowingBiFunction generator, - @Nullable String[] shortcuts) { + @Nullable ThrowingFunction generatorWithoutArguments, + @Nullable ThrowingBiFunction generatorWithArguments, + String @Nullable [] shortcutBeanNames) { this.lookup = lookup; - this.generator = generator; - this.shortcuts = shortcuts; + this.generatorWithoutArguments = generatorWithoutArguments; + this.generatorWithArguments = generatorWithArguments; + this.shortcutBeanNames = shortcutBeanNames; } @@ -114,7 +115,7 @@ private BeanInstanceSupplier(ExecutableLookup lookup, public static BeanInstanceSupplier forConstructor(Class... parameterTypes) { Assert.notNull(parameterTypes, "'parameterTypes' must not be null"); Assert.noNullElements(parameterTypes, "'parameterTypes' must not contain null elements"); - return new BeanInstanceSupplier<>(new ConstructorLookup(parameterTypes), null, null); + return new BeanInstanceSupplier<>(new ConstructorLookup(parameterTypes), null, null, null); } /** @@ -135,7 +136,7 @@ public static BeanInstanceSupplier forFactoryMethod( Assert.noNullElements(parameterTypes, "'parameterTypes' must not contain null elements"); return new BeanInstanceSupplier<>( new FactoryMethodLookup(declaringClass, methodName, parameterTypes), - null, null); + null, null, null); } @@ -151,11 +152,9 @@ ExecutableLookup getLookup() { * instantiate the underlying bean * @return a new {@link BeanInstanceSupplier} instance with the specified generator */ - public BeanInstanceSupplier withGenerator( - ThrowingBiFunction generator) { - + public BeanInstanceSupplier withGenerator(ThrowingBiFunction generator) { Assert.notNull(generator, "'generator' must not be null"); - return new BeanInstanceSupplier<>(this.lookup, generator, this.shortcuts); + return new BeanInstanceSupplier<>(this.lookup, null, generator, this.shortcutBeanNames); } /** @@ -167,70 +166,70 @@ public BeanInstanceSupplier withGenerator( */ public BeanInstanceSupplier withGenerator(ThrowingFunction generator) { Assert.notNull(generator, "'generator' must not be null"); - return new BeanInstanceSupplier<>(this.lookup, - (registeredBean, args) -> generator.apply(registeredBean), this.shortcuts); + 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, args) -> generator.get(), this.shortcuts); - } - - /** - * Return a new {@link BeanInstanceSupplier} instance - * that uses direct bean name injection shortcuts for specific parameters. - * @param beanNames the bean names to use as shortcuts (aligned with the + * Return a new {@link BeanInstanceSupplier} instance that uses + * direct bean name injection shortcuts for specific parameters. + * @param beanNames the bean names to use as shortcut (aligned with the * constructor or factory method parameters) - * @return a new {@link BeanInstanceSupplier} instance - * that uses the shortcuts + * @return a new {@link BeanInstanceSupplier} instance that uses the + * given shortcut bean names + * @since 6.2 */ - public BeanInstanceSupplier withShortcuts(String... beanNames) { - return new BeanInstanceSupplier<>(this.lookup, this.generator, beanNames); + public BeanInstanceSupplier withShortcut(String... beanNames) { + return new BeanInstanceSupplier<>( + this.lookup, this.generatorWithoutArguments, this.generatorWithArguments, beanNames); } + + @SuppressWarnings("unchecked") @Override - public T get(RegisteredBean registeredBean) throws Exception { + public T get(RegisteredBean registeredBean) { Assert.notNull(registeredBean, "'registeredBean' must not be null"); - Executable executable = this.lookup.get(registeredBean); - AutowiredArguments arguments = resolveArguments(registeredBean, executable); - if (this.generator != null) { - return invokeBeanSupplier(executable, () -> this.generator.apply(registeredBean, arguments)); + if (this.generatorWithoutArguments != null) { + Executable executable = getFactoryMethodForGenerator(); + return invokeBeanSupplier(executable, () -> this.generatorWithoutArguments.apply(registeredBean)); } - return invokeBeanSupplier(executable, - () -> instantiate(registeredBean.getBeanFactory(), executable, arguments.toArray())); - } - - private T invokeBeanSupplier(Executable executable, ThrowingSupplier beanSupplier) { - if (!(executable instanceof Method method)) { - return beanSupplier.get(); + else if (this.generatorWithArguments != null) { + Executable executable = getFactoryMethodForGenerator(); + AutowiredArguments arguments = resolveArguments(registeredBean, + executable != null ? executable : this.lookup.get(registeredBean)); + return invokeBeanSupplier(executable, () -> this.generatorWithArguments.apply(registeredBean, arguments)); } - try { - SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(method); - return beanSupplier.get(); - } - finally { - SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(null); + else { + Executable executable = this.lookup.get(registeredBean); + @Nullable Object[] arguments = resolveArguments(registeredBean, executable).toArray(); + return invokeBeanSupplier(executable, () -> (T) instantiate(registeredBean, executable, arguments)); } } - @Nullable @Override - public Method getFactoryMethod() { + public @Nullable Method getFactoryMethod() { + // Cached factory method retrieval for qualifier introspection etc. if (this.lookup instanceof FactoryMethodLookup factoryMethodLookup) { return factoryMethodLookup.get(); } return null; } + 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)) { + return factoryMethodLookup.get(); + } + return null; + } + + private T invokeBeanSupplier(@Nullable Executable executable, ThrowingSupplier beanSupplier) { + if (executable instanceof Method method) { + return SimpleInstantiationStrategy.instantiateWithFactoryMethod(method, beanSupplier); + } + return beanSupplier.get(); + } + /** * Resolve arguments for the specified registered bean. * @param registeredBean the registered bean @@ -242,26 +241,24 @@ AutowiredArguments resolveArguments(RegisteredBean registeredBean) { } private AutowiredArguments resolveArguments(RegisteredBean registeredBean, Executable executable) { - Assert.isInstanceOf(AbstractAutowireCapableBeanFactory.class, registeredBean.getBeanFactory()); - - int startIndex = (executable instanceof Constructor constructor && - ClassUtils.isInnerClass(constructor.getDeclaringClass())) ? 1 : 0; int parameterCount = executable.getParameterCount(); - Object[] resolved = new Object[parameterCount - startIndex]; - Assert.isTrue(this.shortcuts == null || this.shortcuts.length == resolved.length, + @Nullable Object[] resolved = new Object[parameterCount]; + Assert.isTrue(this.shortcutBeanNames == null || this.shortcutBeanNames.length == resolved.length, () -> "'shortcuts' must contain " + resolved.length + " elements"); ValueHolder[] argumentValues = resolveArgumentValues(registeredBean, executable); Set autowiredBeanNames = new LinkedHashSet<>(resolved.length * 2); + int startIndex = (executable instanceof Constructor constructor && + ClassUtils.isInnerClass(constructor.getDeclaringClass())) ? 1 : 0; for (int i = startIndex; i < parameterCount; i++) { MethodParameter parameter = getMethodParameter(executable, i); DependencyDescriptor descriptor = new DependencyDescriptor(parameter, true); - String shortcut = (this.shortcuts != null ? this.shortcuts[i - startIndex] : null); + String shortcut = (this.shortcutBeanNames != null ? this.shortcutBeanNames[i] : null); if (shortcut != null) { descriptor = new ShortcutDependencyDescriptor(descriptor, shortcut); } ValueHolder argumentValue = argumentValues[i]; - resolved[i - startIndex] = resolveAutowiredArgument( + resolved[i] = resolveAutowiredArgument( registeredBean, descriptor, argumentValue, autowiredBeanNames); } registerDependentBeans(registeredBean.getBeanFactory(), registeredBean.getBeanName(), autowiredBeanNames); @@ -289,7 +286,7 @@ private ValueHolder[] resolveArgumentValues(RegisteredBean registeredBean, Execu beanFactory, registeredBean.getBeanName(), beanDefinition, beanFactory.getTypeConverter()); ConstructorArgumentValues values = resolveConstructorArguments( valueResolver, beanDefinition.getConstructorArgumentValues()); - Set usedValueHolders = new HashSet<>(parameters.length); + Set usedValueHolders = CollectionUtils.newHashSet(parameters.length); for (int i = 0; i < parameters.length; i++) { Class parameterType = parameters[i].getType(); String parameterName = (parameters[i].isNamePresent() ? parameters[i].getName() : null); @@ -327,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(); @@ -345,62 +341,30 @@ private Object resolveAutowiredArgument(RegisteredBean registeredBean, Dependenc } } - @SuppressWarnings("unchecked") - private T instantiate(ConfigurableBeanFactory beanFactory, Executable executable, Object[] args) { + private Object instantiate(RegisteredBean registeredBean, Executable executable, @Nullable Object[] args) { if (executable instanceof Constructor constructor) { - try { - return (T) instantiate(constructor, args); - } - catch (Exception ex) { - throw new BeanInstantiationException(constructor, ex.getMessage(), ex); - } + return BeanUtils.instantiateClass(constructor, args); } if (executable instanceof Method method) { + Object target = null; + String factoryBeanName = registeredBean.getMergedBeanDefinition().getFactoryBeanName(); + if (factoryBeanName != null) { + target = registeredBean.getBeanFactory().getBean(factoryBeanName, method.getDeclaringClass()); + } + else if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("Cannot invoke instance method without factoryBeanName: " + method); + } try { - return (T) instantiate(beanFactory, method, args); + ReflectionUtils.makeAccessible(method); + return method.invoke(target, args); } - catch (Exception ex) { + catch (Throwable ex) { throw new BeanInstantiationException(method, ex.getMessage(), ex); } } throw new IllegalStateException("Unsupported executable " + executable.getClass().getName()); } - private Object instantiate(Constructor constructor, Object[] args) throws Exception { - Class declaringClass = constructor.getDeclaringClass(); - if (ClassUtils.isInnerClass(declaringClass)) { - Object enclosingInstance = createInstance(declaringClass.getEnclosingClass()); - args = ObjectUtils.addObjectToArray(args, enclosingInstance, 0); - } - return BeanUtils.instantiateClass(constructor, args); - } - - private Object instantiate(ConfigurableBeanFactory beanFactory, Method method, Object[] args) throws Exception { - Object target = getFactoryMethodTarget(beanFactory, method); - ReflectionUtils.makeAccessible(method); - return method.invoke(target, args); - } - - @Nullable - private Object getFactoryMethodTarget(BeanFactory beanFactory, Method method) { - if (Modifier.isStatic(method.getModifiers())) { - return null; - } - Class declaringClass = method.getDeclaringClass(); - return beanFactory.getBean(declaringClass); - } - - private Object createInstance(Class clazz) throws Exception { - if (!ClassUtils.isInnerClass(clazz)) { - Constructor constructor = clazz.getDeclaredConstructor(); - ReflectionUtils.makeAccessible(constructor); - return constructor.newInstance(); - } - Class enclosingClass = clazz.getEnclosingClass(); - Constructor constructor = clazz.getDeclaredConstructor(enclosingClass); - return constructor.newInstance(createInstance(enclosingClass)); - } - private static String toCommaSeparatedNames(Class... parameterTypes) { return Arrays.stream(parameterTypes).map(Class::getName).collect(Collectors.joining(", ")); @@ -429,12 +393,9 @@ private static class ConstructorLookup extends ExecutableLookup { @Override public Executable get(RegisteredBean registeredBean) { - Class beanClass = registeredBean.getBeanClass(); + Class beanClass = registeredBean.getMergedBeanDefinition().getBeanClass(); try { - Class[] actualParameterTypes = (!ClassUtils.isInnerClass(beanClass)) ? - this.parameterTypes : ObjectUtils.addObjectToArray( - this.parameterTypes, beanClass.getEnclosingClass(), 0); - return beanClass.getDeclaredConstructor(actualParameterTypes); + return beanClass.getDeclaredConstructor(this.parameterTypes); } catch (NoSuchMethodException ex) { throw new IllegalArgumentException( @@ -460,6 +421,8 @@ private static class FactoryMethodLookup extends ExecutableLookup { private final Class[] parameterTypes; + private volatile @Nullable Method resolvedMethod; + FactoryMethodLookup(Class declaringClass, String methodName, Class[] parameterTypes) { this.declaringClass = declaringClass; this.methodName = methodName; @@ -472,8 +435,13 @@ public Executable get(RegisteredBean registeredBean) { } Method get() { - Method method = ReflectionUtils.findMethod(this.declaringClass, this.methodName, this.parameterTypes); - Assert.notNull(method, () -> "%s cannot be found".formatted(this)); + Method method = this.resolvedMethod; + if (method == null) { + method = ReflectionUtils.findMethod( + ClassUtils.getUserClass(this.declaringClass), this.methodName, this.parameterTypes); + Assert.notNull(method, () -> "%s cannot be found".formatted(this)); + this.resolvedMethod = method; + } return method; } 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 5e2c17169610..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 @@ -49,6 +50,15 @@ @FunctionalInterface public interface BeanRegistrationAotProcessor { + /** + * The name of an attribute that can be + * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a + * {@link org.springframework.beans.factory.config.BeanDefinition} to signal + * that its registration should not be processed. + * @since 6.2 + */ + String IGNORE_REGISTRATION_ATTRIBUTE = "aotProcessingIgnoreRegistration"; + /** * Process the given {@link RegisteredBean} instance ahead-of-time and * return a contribution or {@code null}. @@ -63,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/BeanRegistrationCodeFragmentsDecorator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java index 4a493d0d9395..4820770a1022 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.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. @@ -58,39 +58,39 @@ public ClassName getTarget(RegisteredBean registeredBean) { public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationContext, ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) { - return this.delegate.generateNewBeanDefinitionCode(generationContext, - beanType, beanRegistrationCode); + return this.delegate.generateNewBeanDefinitionCode(generationContext, beanType, beanRegistrationCode); } @Override - public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, - Predicate attributeFilter) { + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { return this.delegate.generateSetBeanDefinitionPropertiesCode( generationContext, beanRegistrationCode, beanDefinition, attributeFilter); } @Override - public CodeBlock generateSetBeanInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, CodeBlock instanceSupplierCode, - List postProcessors) { + public CodeBlock generateSetBeanInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + CodeBlock instanceSupplierCode, List postProcessors) { return this.delegate.generateSetBeanInstanceSupplierCode(generationContext, beanRegistrationCode, instanceSupplierCode, postProcessors); } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + boolean allowDirectSupplierShortcut) { return this.delegate.generateInstanceSupplierCode(generationContext, beanRegistrationCode, allowDirectSupplierShortcut); } @Override - public CodeBlock generateReturnCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode) { + public CodeBlock generateReturnCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { return this.delegate.generateReturnCode(generationContext, beanRegistrationCode); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationKey.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationKey.java deleted file mode 100644 index ffd3f99c9c7d..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationKey.java +++ /dev/null @@ -1,28 +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.beans.factory.aot; - -/** - * Record class holding key information for beans registered in a bean factory. - * - * @param beanName the name of the registered bean - * @param beanClass the type of the registered bean - * @author Brian Clozel - * @since 6.0.8 - */ -record BeanRegistrationKey(String beanName, Class beanClass) { -} 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 5960d80952d1..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 @@ -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,7 @@ package org.springframework.beans.factory.aot; -import java.util.Map; +import java.util.List; import javax.lang.model.element.Modifier; @@ -26,14 +26,15 @@ 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; import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.MethodSpec; -import org.springframework.util.ClassUtils; /** * AOT contribution from a {@link BeanRegistrationsAotProcessor} used to @@ -51,10 +52,13 @@ class BeanRegistrationsAotContribution private static final String BEAN_FACTORY_PARAMETER_NAME = "beanFactory"; - private final Map registrations; + private static final ArgumentCodeGenerator argumentCodeGenerator = ArgumentCodeGenerator + .of(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME); + private final List registrations; - BeanRegistrationsAotContribution(Map registrations) { + + BeanRegistrationsAotContribution(List registrations) { this.registrations = registrations; } @@ -69,8 +73,8 @@ public void applyTo(GenerationContext generationContext, type.addModifiers(Modifier.PUBLIC); }); BeanRegistrationsCodeGenerator codeGenerator = new BeanRegistrationsCodeGenerator(generatedClass); - GeneratedMethod generatedBeanDefinitionsMethod = codeGenerator.getMethods().add("registerBeanDefinitions", method -> - generateRegisterBeanDefinitionsMethod(method, generationContext, codeGenerator)); + GeneratedMethod generatedBeanDefinitionsMethod = new BeanDefinitionsRegistrationGenerator( + generationContext, codeGenerator, this.registrations).generateRegisterBeanDefinitionsMethod(); beanFactoryInitializationCode.addInitializer(generatedBeanDefinitionsMethod.toMethodReference()); GeneratedMethod generatedAliasesMethod = codeGenerator.getMethods().add("registerAliases", this::generateRegisterAliasesMethod); @@ -78,66 +82,42 @@ public void applyTo(GenerationContext generationContext, generateRegisterHints(generationContext.getRuntimeHints(), this.registrations); } - private void generateRegisterBeanDefinitionsMethod(MethodSpec.Builder method, - GenerationContext generationContext, BeanRegistrationsCode beanRegistrationsCode) { - - method.addJavadoc("Register the bean definitions."); - method.addModifiers(Modifier.PUBLIC); - method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME); - CodeBlock.Builder code = CodeBlock.builder(); - this.registrations.forEach((registeredBean, registration) -> { - MethodReference beanDefinitionMethod = registration.methodGenerator - .generateBeanDefinitionMethod(generationContext, beanRegistrationsCode); - CodeBlock methodInvocation = beanDefinitionMethod.toInvokeCodeBlock( - ArgumentCodeGenerator.none(), beanRegistrationsCode.getClassName()); - code.addStatement("$L.registerBeanDefinition($S, $L)", - BEAN_FACTORY_PARAMETER_NAME, registeredBean.beanName(), methodInvocation); - }); - method.addCode(code.build()); - } - private void generateRegisterAliasesMethod(MethodSpec.Builder method) { method.addJavadoc("Register the aliases."); method.addModifiers(Modifier.PUBLIC); method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME); CodeBlock.Builder code = CodeBlock.builder(); - this.registrations.forEach((registeredBean, registration) -> { - for (String alias : registration.aliases) { + this.registrations.forEach(registration -> { + for (String alias : registration.aliases()) { code.addStatement("$L.registerAlias($S, $S)", BEAN_FACTORY_PARAMETER_NAME, - registeredBean.beanName(), alias); + registration.beanName(), alias); } }); method.addCode(code.build()); } - private void generateRegisterHints(RuntimeHints runtimeHints, Map registrations) { - registrations.keySet().forEach(beanRegistrationKey -> { + private void generateRegisterHints(RuntimeHints runtimeHints, List registrations) { + registrations.forEach(registration -> { ReflectionHints hints = runtimeHints.reflection(); - Class beanClass = beanRegistrationKey.beanClass(); - hints.registerType(beanClass, MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS); - introspectPublicMethodsOnAllInterfaces(hints, beanClass); + Class beanClass = registration.registeredBean.getBeanClass(); + hints.registerType(beanClass); + hints.registerForInterfaces(beanClass, TypeHint.Builder::withMembers); }); } - private void introspectPublicMethodsOnAllInterfaces(ReflectionHints hints, Class type) { - Class currentClass = type; - while (currentClass != null && currentClass != Object.class) { - for (Class interfaceType : currentClass.getInterfaces()) { - if (!ClassUtils.isJavaLanguageInterface(interfaceType)) { - hints.registerType(interfaceType, MemberCategory.INTROSPECT_PUBLIC_METHODS); - introspectPublicMethodsOnAllInterfaces(hints, interfaceType); - } - } - currentClass = currentClass.getSuperclass(); - } - } - /** * Gather the necessary information to register a particular bean. + * @param registeredBean the bean to register * @param methodGenerator the {@link BeanDefinitionMethodGenerator} to use * @param aliases the bean aliases, if any */ - record Registration(BeanDefinitionMethodGenerator methodGenerator, String[] aliases) {} + record Registration(RegisteredBean registeredBean, BeanDefinitionMethodGenerator methodGenerator, String[] aliases) { + + String beanName() { + return this.registeredBean.getBeanName(); + } + + } /** @@ -164,4 +144,89 @@ public GeneratedMethods getMethods() { } + static final class BeanDefinitionsRegistrationGenerator { + + private final GenerationContext generationContext; + + private final BeanRegistrationsCodeGenerator codeGenerator; + + private final List registrations; + + + BeanDefinitionsRegistrationGenerator(GenerationContext generationContext, + BeanRegistrationsCodeGenerator codeGenerator, List registrations) { + + this.generationContext = generationContext; + this.codeGenerator = codeGenerator; + this.registrations = registrations; + } + + + GeneratedMethod generateRegisterBeanDefinitionsMethod() { + return this.codeGenerator.getMethods().add("registerBeanDefinitions", method -> { + method.addJavadoc("Register the bean definitions."); + method.addModifiers(Modifier.PUBLIC); + method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME); + if (this.registrations.size() <= 1000) { + generateRegisterBeanDefinitionMethods(method, this.registrations); + } + else { + Builder code = CodeBlock.builder(); + code.add("// Registration is sliced to avoid exceeding size limit\n"); + int index = 0; + int end = 0; + while (end < this.registrations.size()) { + int start = index * 1000; + end = Math.min(start + 1000, this.registrations.size()); + GeneratedMethod sliceMethod = generateSliceMethod(start, end); + code.addStatement(sliceMethod.toMethodReference().toInvokeCodeBlock( + argumentCodeGenerator, this.codeGenerator.getClassName())); + index++; + } + method.addCode(code.build()); + } + }); + } + + private GeneratedMethod generateSliceMethod(int start, int end) { + String description = "Register the bean definitions from %s to %s.".formatted(start, end - 1); + List slice = this.registrations.subList(start, end); + return this.codeGenerator.getMethods().add("registerBeanDefinitions", method -> { + method.addJavadoc(description); + method.addModifiers(Modifier.PRIVATE); + method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME); + generateRegisterBeanDefinitionMethods(method, slice); + }); + } + + + private void generateRegisterBeanDefinitionMethods(MethodSpec.Builder method, + Iterable registrations) { + + CodeBlock.Builder code = CodeBlock.builder(); + registrations.forEach(registration -> { + try { + CodeBlock methodInvocation = generateBeanRegistration(registration); + code.addStatement("$L.registerBeanDefinition($S, $L)", + BEAN_FACTORY_PARAMETER_NAME, registration.beanName(), methodInvocation); + } + catch (AotException ex) { + throw ex; + } + catch (Exception ex) { + throw new AotBeanProcessingException(registration.registeredBean, + "failed to generate code for bean definition", ex); + } + }); + method.addCode(code.build()); + } + + private CodeBlock generateBeanRegistration(Registration registration) { + MethodReference beanDefinitionMethod = registration.methodGenerator + .generateBeanDefinitionMethod(this.generationContext, this.codeGenerator); + return beanDefinitionMethod.toInvokeCodeBlock( + ArgumentCodeGenerator.none(), this.codeGenerator.getClassName()); + } + } + } 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 05df3e7a7c8b..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 @@ -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,13 +16,14 @@ package org.springframework.beans.factory.aot; -import java.util.LinkedHashMap; -import java.util.Map; +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,19 +38,18 @@ class BeanRegistrationsAotProcessor implements BeanFactoryInitializationAotProcessor { @Override - @Nullable - public BeanRegistrationsAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + public @Nullable BeanRegistrationsAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory(beanFactory); - Map registrations = new LinkedHashMap<>(); + List registrations = new ArrayList<>(); for (String beanName : beanFactory.getBeanDefinitionNames()) { RegisteredBean registeredBean = RegisteredBean.of(beanFactory, beanName); BeanDefinitionMethodGenerator beanDefinitionMethodGenerator = beanDefinitionMethodGeneratorFactory.getBeanDefinitionMethodGenerator(registeredBean); if (beanDefinitionMethodGenerator != null) { - registrations.put(new BeanRegistrationKey(beanName, registeredBean.getBeanClass()), - new Registration(beanDefinitionMethodGenerator, beanFactory.getAliases(beanName))); + registrations.add(new Registration(registeredBean, beanDefinitionMethodGenerator, + beanFactory.getAliases(beanName))); } } 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 ba0de5056e28..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 @@ -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,13 +20,19 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; -import java.util.StringJoiner; +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; import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.MethodSpec; -import org.springframework.lang.Nullable; +import org.springframework.javapoet.TypeSpec; +import org.springframework.util.ClassUtils; /** * Helper class to register warnings that the compiler may trigger on @@ -36,7 +42,7 @@ * @since 6.1 * @see SuppressWarnings */ -class CodeWarnings { +public class CodeWarnings { private final Set warnings = new LinkedHashSet<>(); @@ -57,7 +63,7 @@ public void register(String warning) { */ public CodeWarnings detectDeprecation(AnnotatedElement... elements) { for (AnnotatedElement element : elements) { - register(element.getAnnotation(Deprecated.class)); + registerDeprecationIfNecessary(element); } return this; } @@ -72,15 +78,57 @@ public CodeWarnings detectDeprecation(Stream elements) { return this; } + /** + * Detect the presence of {@link Deprecated} on the signature of the + * specified {@link ResolvableType}. + * @param resolvableType a type signature + * @return {@code this} instance + * @since 6.1.8 + */ + public CodeWarnings detectDeprecation(ResolvableType resolvableType) { + if (ResolvableType.NONE.equals(resolvableType)) { + return this; + } + Class type = ClassUtils.getUserClass(resolvableType.toClass()); + detectDeprecation(type); + if (resolvableType.hasGenerics() && !resolvableType.hasUnresolvableGenerics()) { + for (ResolvableType generic : resolvableType.getGenerics()) { + detectDeprecation(generic); + } + } + return this; + } + /** * Include {@link SuppressWarnings} on the specified method if necessary. * @param method the method to update */ public void suppress(MethodSpec.Builder method) { - if (this.warnings.isEmpty()) { - return; + suppress(annotationBuilder -> method.addAnnotation(annotationBuilder.build())); + } + + /** + * Include {@link SuppressWarnings} on the specified type if necessary. + * @param type the type to update + */ + public void suppress(TypeSpec.Builder type) { + suppress(annotationBuilder -> type.addAnnotation(annotationBuilder.build())); + } + + /** + * Consume the builder for {@link SuppressWarnings} if necessary. If this + * instance has no warnings registered, the consumer is not invoked. + * @param annotationSpec a consumer of the {@link AnnotationSpec.Builder} + * @see MethodSpec.Builder#addAnnotation(AnnotationSpec) + * @see TypeSpec.Builder#addAnnotation(AnnotationSpec) + * @see FieldSpec.Builder#addAnnotation(AnnotationSpec) + */ + protected void suppress(Consumer annotationSpec) { + if (!this.warnings.isEmpty()) { + Builder annotation = AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", generateValueCode()); + annotationSpec.accept(annotation); } - method.addAnnotation(buildAnnotationSpec()); } /** @@ -91,6 +139,16 @@ protected Set getWarnings() { return Collections.unmodifiableSet(this.warnings); } + private void registerDeprecationIfNecessary(@Nullable AnnotatedElement element) { + if (element == null) { + return; + } + register(element.getAnnotation(Deprecated.class)); + if (element instanceof Class type) { + registerDeprecationIfNecessary(type.getEnclosingClass()); + } + } + private void register(@Nullable Deprecated annotation) { if (annotation != null) { if (annotation.forRemoval()) { @@ -102,11 +160,6 @@ private void register(@Nullable Deprecated annotation) { } } - private AnnotationSpec buildAnnotationSpec() { - return AnnotationSpec.builder(SuppressWarnings.class) - .addMember("value", generateValueCode()).build(); - } - private CodeBlock generateValueCode() { if (this.warnings.size() == 1) { return CodeBlock.of("$S", this.warnings.iterator().next()); @@ -118,9 +171,7 @@ private CodeBlock generateValueCode() { @Override public String toString() { - return new StringJoiner(", ", CodeWarnings.class.getSimpleName(), "") - .add(this.warnings.toString()) - .toString(); + return CodeWarnings.class.getSimpleName() + this.warnings; } } 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 90c4f8acf25a..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 @@ -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. @@ -17,67 +17,71 @@ package org.springframework.beans.factory.aot; import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; import java.lang.reflect.Modifier; import java.util.List; 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; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerator.Delegate; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.aot.AotServices.Loader; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.InstanceSupplier; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.ResolvableType; 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; /** - * Internal {@link BeanRegistrationCodeFragments} implementation used by - * default. + * Internal {@link BeanRegistrationCodeFragments} implementation used by default. * * @author Phillip Webb * @author Stephane Nicoll */ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragments { + private static final ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults(); + private final BeanRegistrationsCode beanRegistrationsCode; private final RegisteredBean registeredBean; private final BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory; - private final Supplier constructorOrFactoryMethod; + private final Supplier instantiationDescriptor; - DefaultBeanRegistrationCodeFragments(BeanRegistrationsCode beanRegistrationsCode, - RegisteredBean registeredBean, + DefaultBeanRegistrationCodeFragments( + BeanRegistrationsCode beanRegistrationsCode, RegisteredBean registeredBean, BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory) { this.beanRegistrationsCode = beanRegistrationsCode; this.registeredBean = registeredBean; this.beanDefinitionMethodGeneratorFactory = beanDefinitionMethodGeneratorFactory; - this.constructorOrFactoryMethod = SingletonSupplier.of(registeredBean::resolveConstructorOrFactoryMethod); + this.instantiationDescriptor = SingletonSupplier.of(registeredBean::resolveInstantiationDescriptor); } @Override public ClassName getTarget(RegisteredBean registeredBean) { if (hasInstanceSupplier()) { - throw new IllegalStateException("Default code generation is not supported for bean definitions " - + "declaring an instance supplier callback: " + registeredBean.getMergedBeanDefinition()); + throw new AotBeanProcessingException(registeredBean, "instance supplier is not supported"); } - Class target = extractDeclaringClass(registeredBean.getBeanType(), this.constructorOrFactoryMethod.get()); + Class target = extractDeclaringClass(registeredBean, this.instantiationDescriptor.get()); while (target.getName().startsWith("java.") && registeredBean.isInnerBean()) { RegisteredBean parent = registeredBean.getParent(); Assert.state(parent != null, "No parent available for inner bean"); @@ -86,21 +90,19 @@ public ClassName getTarget(RegisteredBean registeredBean) { return (target.isArray() ? ClassName.get(target.getComponentType()) : ClassName.get(target)); } - private Class extractDeclaringClass(ResolvableType beanType, Executable executable) { - Class declaringClass = ClassUtils.getUserClass(executable.getDeclaringClass()); - if (executable instanceof Constructor - && AccessControl.forMember(executable).isPublic() - && FactoryBean.class.isAssignableFrom(declaringClass)) { - return extractTargetClassFromFactoryBean(declaringClass, beanType); + private Class extractDeclaringClass(RegisteredBean registeredBean, InstantiationDescriptor instantiationDescriptor) { + Class declaringClass = ClassUtils.getUserClass(instantiationDescriptor.targetClass()); + if (instantiationDescriptor.executable() instanceof Constructor ctor && + AccessControl.forMember(ctor).isPublic() && FactoryBean.class.isAssignableFrom(declaringClass)) { + return extractTargetClassFromFactoryBean(declaringClass, registeredBean.getBeanType()); } - return executable.getDeclaringClass(); + return declaringClass; } /** * Extract the target class of a public {@link FactoryBean} based on its * constructor. If the implementation does not resolve the target class - * because it itself uses a generic, attempt to extract it from the - * bean type. + * because it itself uses a generic, attempt to extract it from the bean type. * @param factoryBeanType the factory bean type * @param beanType the bean type * @return the target class to use @@ -121,17 +123,15 @@ public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationConte ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) { CodeBlock.Builder code = CodeBlock.builder(); - RootBeanDefinition mergedBeanDefinition = this.registeredBean.getMergedBeanDefinition(); - Class beanClass = (mergedBeanDefinition.hasBeanClass() - ? ClassUtils.getUserClass(mergedBeanDefinition.getBeanClass()) : null); + RootBeanDefinition mbd = this.registeredBean.getMergedBeanDefinition(); + Class beanClass = (mbd.hasBeanClass() ? ClassUtils.getUserClass(mbd.getBeanClass()) : null); CodeBlock beanClassCode = generateBeanClassCode( beanRegistrationCode.getClassName().packageName(), (beanClass != null ? beanClass : beanType.toClass())); code.addStatement("$T $L = new $T($L)", RootBeanDefinition.class, BEAN_DEFINITION_VARIABLE, RootBeanDefinition.class, beanClassCode); if (targetTypeNecessary(beanType, beanClass)) { - code.addStatement("$L.setTargetType($L)", BEAN_DEFINITION_VARIABLE, - generateBeanTypeCode(beanType)); + code.addStatement("$L.setTargetType($L)", BEAN_DEFINITION_VARIABLE, generateBeanTypeCode(beanType)); } return code.build(); } @@ -147,39 +147,37 @@ private CodeBlock generateBeanClassCode(String targetPackage, Class beanClass private CodeBlock generateBeanTypeCode(ResolvableType beanType) { if (!beanType.hasGenerics()) { - return CodeBlock.of("$T.class", ClassUtils.getUserClass(beanType.toClass())); + return valueCodeGenerator.generateCode(ClassUtils.getUserClass(beanType.toClass())); } - return ResolvableTypeCodeGenerator.generateCode(beanType); + return valueCodeGenerator.generateCode(beanType); } private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class beanClass) { if (beanType.hasGenerics()) { return true; } - if (beanClass != null - && this.registeredBean.getMergedBeanDefinition().getFactoryMethodName() != null) { + if (beanClass != null && this.registeredBean.getMergedBeanDefinition().getFactoryMethodName() != null) { return true; } - return (beanClass != null && !beanType.toClass().equals(beanClass)); + return (beanClass != null && !beanType.toClass().equals(ClassUtils.getUserClass(beanClass))); } @Override public CodeBlock generateSetBeanDefinitionPropertiesCode( - GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, - Predicate attributeFilter) { + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { + + Loader loader = AotServices.factories(this.registeredBean.getBeanFactory().getBeanClassLoader()); + List additionalDelegates = loader.load(Delegate.class).asList(); return new BeanDefinitionPropertiesCodeGenerator( generationContext.getRuntimeHints(), attributeFilter, - beanRegistrationCode.getMethods(), + beanRegistrationCode.getMethods(), additionalDelegates, (name, value) -> generateValueCode(generationContext, name, value)) .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 @@ -192,8 +190,7 @@ protected CodeBlock generateValueCode(GenerationContext generationContext, 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); } @@ -205,9 +202,8 @@ private RegisteredBean getInnerRegisteredBean(Object value) { @Override public CodeBlock generateSetBeanInstanceSupplierCode( - GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, CodeBlock instanceSupplierCode, - List postProcessors) { + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + CodeBlock instanceSupplierCode, List postProcessors) { CodeBlock.Builder code = CodeBlock.builder(); if (postProcessors.isEmpty()) { @@ -227,20 +223,21 @@ public CodeBlock generateSetBeanInstanceSupplierCode( } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + boolean allowDirectSupplierShortcut) { + if (hasInstanceSupplier()) { - throw new IllegalStateException("Default code generation is not supported for bean definitions declaring " - + "an instance supplier callback: " + this.registeredBean.getMergedBeanDefinition()); + throw new AotBeanProcessingException(this.registeredBean, "instance supplier is not supported"); } return new InstanceSupplierCodeGenerator(generationContext, beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut) - .generateCode(this.registeredBean, this.constructorOrFactoryMethod.get()); + .generateCode(this.registeredBean, this.instantiationDescriptor.get()); } @Override - public CodeBlock generateReturnCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode) { + public CodeBlock generateReturnCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { CodeBlock.Builder code = CodeBlock.builder(); code.addStatement("return $L", BEAN_DEFINITION_VARIABLE); 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 3087d8b481a9..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 @@ -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,6 @@ import java.lang.reflect.Executable; import java.lang.reflect.Member; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.lang.reflect.Proxy; import java.util.Arrays; @@ -30,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; @@ -46,6 +46,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.InstanceSupplier; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; @@ -66,7 +67,7 @@ *

Generated code is usually a method reference that generates the * {@link BeanInstanceSupplier}, but some shortcut can be used as well such as: *

- * {@code InstanceSupplier.of(TheGeneratedClass::getMyBeanInstance);}
+ * InstanceSupplier.of(TheGeneratedClass::getMyBeanInstance);
  * 
* * @author Phillip Webb @@ -82,9 +83,8 @@ public class InstanceSupplierCodeGenerator { private static final String ARGS_PARAMETER_NAME = "args"; - private static final javax.lang.model.element.Modifier[] PRIVATE_STATIC = { - javax.lang.model.element.Modifier.PRIVATE, - javax.lang.model.element.Modifier.STATIC }; + private static final javax.lang.model.element.Modifier[] PRIVATE_STATIC = + {javax.lang.model.element.Modifier.PRIVATE, javax.lang.model.element.Modifier.STATIC}; private static final CodeBlock NO_ARGS = CodeBlock.of(""); @@ -99,7 +99,7 @@ public class InstanceSupplierCodeGenerator { /** - * Create a new instance. + * Create a new generator instance. * @param generationContext the generation context * @param className the class name of the bean to instantiate * @param generatedMethods the generated methods @@ -120,176 +120,174 @@ public InstanceSupplierCodeGenerator(GenerationContext generationContext, * @param registeredBean the bean to handle * @param constructorOrFactoryMethod the executable to use to create the bean * @return the generated code + * @deprecated in favor of {@link #generateCode(RegisteredBean, InstantiationDescriptor)} */ + @Deprecated(since = "6.1.7") public CodeBlock generateCode(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { + return generateCode(registeredBean, new InstantiationDescriptor( + constructorOrFactoryMethod, constructorOrFactoryMethod.getDeclaringClass())); + } + + /** + * Generate the instance supplier code. + * @param registeredBean the bean to handle + * @param instantiationDescriptor the executable to use to create the bean + * @return the generated code + * @since 6.1.7 + */ + public CodeBlock generateCode(RegisteredBean registeredBean, InstantiationDescriptor instantiationDescriptor) { + Executable constructorOrFactoryMethod = instantiationDescriptor.executable(); registerRuntimeHintsIfNecessary(registeredBean, constructorOrFactoryMethod); if (constructorOrFactoryMethod instanceof Constructor constructor) { return generateCodeForConstructor(registeredBean, constructor); } - if (constructorOrFactoryMethod instanceof Method method) { - return generateCodeForFactoryMethod(registeredBean, method); + if (constructorOrFactoryMethod instanceof Method method && !KotlinDetector.isSuspendingFunction(method)) { + return generateCodeForFactoryMethod(registeredBean, method, instantiationDescriptor.targetClass()); } - throw new IllegalStateException( - "No suitable executor found for " + registeredBean.getBeanName()); + throw new AotBeanProcessingException(registeredBean, "no suitable constructor or factory method found"); } private void registerRuntimeHintsIfNecessary(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { if (registeredBean.getBeanFactory() instanceof DefaultListableBeanFactory dlbf) { RuntimeHints runtimeHints = this.generationContext.getRuntimeHints(); ProxyRuntimeHintsRegistrar registrar = new ProxyRuntimeHintsRegistrar(dlbf.getAutowireCandidateResolver()); - if (constructorOrFactoryMethod instanceof Method method) { - registrar.registerRuntimeHints(runtimeHints, method); - } - else if (constructorOrFactoryMethod instanceof Constructor constructor) { - registrar.registerRuntimeHints(runtimeHints, constructor); - } + registrar.registerRuntimeHints(runtimeHints, constructorOrFactoryMethod); } } private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Constructor constructor) { - String beanName = registeredBean.getBeanName(); - Class beanClass = registeredBean.getBeanClass(); - Class declaringClass = constructor.getDeclaringClass(); - boolean dependsOnBean = ClassUtils.isInnerClass(declaringClass); - - Visibility accessVisibility = getAccessVisibility(registeredBean, constructor); - if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(beanClass)) { - return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, - dependsOnBean, hints -> hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + ConstructorDescriptor descriptor = new ConstructorDescriptor( + registeredBean.getBeanName(), constructor, registeredBean.getBeanClass()); + + Class publicType = descriptor.publicType(); + if (KotlinDetector.isKotlinType(publicType) && KotlinDelegate.hasConstructorWithOptionalParameter(publicType)) { + return generateCodeForInaccessibleConstructor(descriptor, + hints -> hints.registerType(publicType, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); } - else if (accessVisibility != Visibility.PRIVATE) { - return generateCodeForAccessibleConstructor(beanName, beanClass, constructor, - dependsOnBean, declaringClass); + + if (!isVisible(constructor, constructor.getDeclaringClass())) { + return generateCodeForInaccessibleConstructor(descriptor, + hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE)); } - return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean, - hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE)); + return generateCodeForAccessibleConstructor(descriptor); } - private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class beanClass, - Constructor constructor, boolean dependsOnBean, Class declaringClass) { - - this.generationContext.getRuntimeHints().reflection().registerConstructor( - constructor, ExecutableMode.INTROSPECT); + private CodeBlock generateCodeForAccessibleConstructor(ConstructorDescriptor descriptor) { + Constructor constructor = descriptor.constructor(); + this.generationContext.getRuntimeHints().reflection().registerType(constructor.getDeclaringClass()); - if (!dependsOnBean && constructor.getParameterCount() == 0) { + if (constructor.getParameterCount() == 0) { if (!this.allowDirectSupplierShortcut) { - return CodeBlock.of("$T.using($T::new)", InstanceSupplier.class, declaringClass); + return CodeBlock.of("$T.using($T::new)", InstanceSupplier.class, descriptor.actualType()); } if (!isThrowingCheckedException(constructor)) { - return CodeBlock.of("$T::new", declaringClass); + return CodeBlock.of("$T::new", descriptor.actualType()); } - return CodeBlock.of("$T.of($T::new)", ThrowingSupplier.class, declaringClass); + return CodeBlock.of("$T.of($T::new)", ThrowingSupplier.class, descriptor.actualType()); } GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> - buildGetInstanceMethodForConstructor(method, beanName, beanClass, constructor, - declaringClass, dependsOnBean, PRIVATE_STATIC)); + buildGetInstanceMethodForConstructor(method, descriptor, PRIVATE_STATIC)); return generateReturnStatement(generatedMethod); } - private CodeBlock generateCodeForInaccessibleConstructor(String beanName, Class beanClass, - Constructor constructor, boolean dependsOnBean, Consumer hints) { + private CodeBlock generateCodeForInaccessibleConstructor(ConstructorDescriptor descriptor, + Consumer hints) { + Constructor constructor = descriptor.constructor(); CodeWarnings codeWarnings = new CodeWarnings(); - codeWarnings.detectDeprecation(beanClass, constructor) + codeWarnings.detectDeprecation(constructor.getDeclaringClass(), constructor) .detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType)); hints.accept(this.generationContext.getRuntimeHints().reflection()); GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> { - method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); + method.addJavadoc("Get the bean instance supplier for '$L'.", descriptor.beanName()); method.addModifiers(PRIVATE_STATIC); codeWarnings.suppress(method); - method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass)); - int parameterOffset = (!dependsOnBean) ? 0 : 1; - method.addStatement(generateResolverForConstructor(beanClass, constructor, parameterOffset)); + method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, descriptor.publicType())); + method.addStatement(generateResolverForConstructor(descriptor)); }); return generateReturnStatement(generatedMethod); } - private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, - String beanName, Class beanClass, Constructor constructor, Class declaringClass, - boolean dependsOnBean, javax.lang.model.element.Modifier... modifiers) { + private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, ConstructorDescriptor descriptor, + javax.lang.model.element.Modifier... modifiers) { + + Constructor constructor = descriptor.constructor(); + Class publicType = descriptor.publicType(); + Class actualType = descriptor.actualType(); CodeWarnings codeWarnings = new CodeWarnings(); - codeWarnings.detectDeprecation(beanClass, constructor, declaringClass) + codeWarnings.detectDeprecation(actualType, constructor) .detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType)); - method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); + method.addJavadoc("Get the bean instance supplier for '$L'.", descriptor.beanName()); method.addModifiers(modifiers); codeWarnings.suppress(method); - method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass)); + method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, publicType)); - int parameterOffset = (!dependsOnBean) ? 0 : 1; CodeBlock.Builder code = CodeBlock.builder(); - code.add(generateResolverForConstructor(beanClass, constructor, parameterOffset)); + code.add(generateResolverForConstructor(descriptor)); boolean hasArguments = constructor.getParameterCount() > 0; + boolean onInnerClass = ClassUtils.isInnerClass(actualType); CodeBlock arguments = hasArguments ? - new AutowiredArgumentsCodeGenerator(declaringClass, constructor) - .generateCode(constructor.getParameterTypes(), parameterOffset) - : NO_ARGS; + new AutowiredArgumentsCodeGenerator(actualType, constructor) + .generateCode(constructor.getParameterTypes(), (onInnerClass ? 1 : 0)) : NO_ARGS; - CodeBlock newInstance = generateNewInstanceCodeForConstructor(dependsOnBean, declaringClass, arguments); + CodeBlock newInstance = generateNewInstanceCodeForConstructor(actualType, arguments); code.add(generateWithGeneratorCode(hasArguments, newInstance)); method.addStatement(code.build()); } - private CodeBlock generateResolverForConstructor(Class beanClass, - Constructor constructor, int parameterOffset) { - - CodeBlock parameterTypes = generateParameterTypesCode(constructor.getParameterTypes(), parameterOffset); - return CodeBlock.of("return $T.<$T>forConstructor($L)", BeanInstanceSupplier.class, beanClass, parameterTypes); + private CodeBlock generateResolverForConstructor(ConstructorDescriptor descriptor) { + CodeBlock parameterTypes = generateParameterTypesCode(descriptor.constructor().getParameterTypes()); + return CodeBlock.of("return $T.<$T>forConstructor($L)", BeanInstanceSupplier.class, + descriptor.publicType(), parameterTypes); } - private CodeBlock generateNewInstanceCodeForConstructor(boolean dependsOnBean, - Class declaringClass, CodeBlock args) { - - if (!dependsOnBean) { - return CodeBlock.of("new $T($L)", declaringClass, args); + private CodeBlock generateNewInstanceCodeForConstructor(Class declaringClass, CodeBlock args) { + if (ClassUtils.isInnerClass(declaringClass)) { + return CodeBlock.of("$L.getBeanFactory().getBean($T.class).new $L($L)", + REGISTERED_BEAN_PARAMETER_NAME, declaringClass.getEnclosingClass(), + declaringClass.getSimpleName(), args); } - - return CodeBlock.of("$L.getBeanFactory().getBean($T.class).new $L($L)", - REGISTERED_BEAN_PARAMETER_NAME, declaringClass.getEnclosingClass(), - declaringClass.getSimpleName(), args); + return CodeBlock.of("new $T($L)", declaringClass, args); } - private CodeBlock generateCodeForFactoryMethod(RegisteredBean registeredBean, Method factoryMethod) { - String beanName = registeredBean.getBeanName(); - Class declaringClass = ClassUtils.getUserClass(factoryMethod.getDeclaringClass()); - boolean dependsOnBean = !Modifier.isStatic(factoryMethod.getModifiers()); + private CodeBlock generateCodeForFactoryMethod( + RegisteredBean registeredBean, Method factoryMethod, Class targetClass) { - Visibility accessVisibility = getAccessVisibility(registeredBean, factoryMethod); - if (accessVisibility != Visibility.PRIVATE) { - return generateCodeForAccessibleFactoryMethod( - beanName, factoryMethod, declaringClass, dependsOnBean); + if (!isVisible(factoryMethod, targetClass)) { + return generateCodeForInaccessibleFactoryMethod(registeredBean.getBeanName(), factoryMethod, targetClass); } - return generateCodeForInaccessibleFactoryMethod(beanName, factoryMethod, declaringClass); + return generateCodeForAccessibleFactoryMethod(registeredBean.getBeanName(), factoryMethod, targetClass, + registeredBean.getMergedBeanDefinition().getFactoryBeanName()); } private CodeBlock generateCodeForAccessibleFactoryMethod(String beanName, - Method factoryMethod, Class declaringClass, boolean dependsOnBean) { + Method factoryMethod, Class targetClass, @Nullable String factoryBeanName) { - this.generationContext.getRuntimeHints().reflection().registerMethod( - factoryMethod, ExecutableMode.INTROSPECT); + this.generationContext.getRuntimeHints().reflection().registerType(factoryMethod.getDeclaringClass()); - if (!dependsOnBean && factoryMethod.getParameterCount() == 0) { + if (factoryBeanName == null && factoryMethod.getParameterCount() == 0) { Class suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType()); CodeBlock.Builder code = CodeBlock.builder(); code.add("$T.<$T>forFactoryMethod($T.class, $S)", BeanInstanceSupplier.class, - suppliedType, declaringClass, factoryMethod.getName()); + suppliedType, targetClass, factoryMethod.getName()); code.add(".withGenerator(($L) -> $T.$L())", REGISTERED_BEAN_PARAMETER_NAME, - declaringClass, factoryMethod.getName()); + ClassUtils.getUserClass(targetClass), factoryMethod.getName()); return code.build(); } GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> buildGetInstanceMethodForFactoryMethod(method, beanName, factoryMethod, - declaringClass, dependsOnBean, PRIVATE_STATIC)); + targetClass, factoryBeanName, PRIVATE_STATIC)); return generateReturnStatement(getInstanceMethod); } private CodeBlock generateCodeForInaccessibleFactoryMethod( - String beanName, Method factoryMethod, Class declaringClass) { + String beanName, Method factoryMethod, Class targetClass) { this.generationContext.getRuntimeHints().reflection().registerMethod(factoryMethod, ExecutableMode.INVOKE); GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> { @@ -298,19 +296,19 @@ private CodeBlock generateCodeForInaccessibleFactoryMethod( method.addModifiers(PRIVATE_STATIC); method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, suppliedType)); method.addStatement(generateInstanceSupplierForFactoryMethod( - factoryMethod, suppliedType, declaringClass, factoryMethod.getName())); + factoryMethod, suppliedType, targetClass, factoryMethod.getName())); }); return generateReturnStatement(getInstanceMethod); } private void buildGetInstanceMethodForFactoryMethod(MethodSpec.Builder method, - String beanName, Method factoryMethod, Class declaringClass, - boolean dependsOnBean, javax.lang.model.element.Modifier... modifiers) { + String beanName, Method factoryMethod, Class targetClass, + @Nullable String factoryBeanName, javax.lang.model.element.Modifier... modifiers) { String factoryMethodName = factoryMethod.getName(); Class suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType()); CodeWarnings codeWarnings = new CodeWarnings(); - codeWarnings.detectDeprecation(declaringClass, factoryMethod, suppliedType) + codeWarnings.detectDeprecation(ClassUtils.getUserClass(targetClass), factoryMethod, suppliedType) .detectDeprecation(Arrays.stream(factoryMethod.getParameters()).map(Parameter::getType)); method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); @@ -320,41 +318,40 @@ private void buildGetInstanceMethodForFactoryMethod(MethodSpec.Builder method, CodeBlock.Builder code = CodeBlock.builder(); code.add(generateInstanceSupplierForFactoryMethod( - factoryMethod, suppliedType, declaringClass, factoryMethodName)); + factoryMethod, suppliedType, targetClass, factoryMethodName)); boolean hasArguments = factoryMethod.getParameterCount() > 0; CodeBlock arguments = hasArguments ? - new AutowiredArgumentsCodeGenerator(declaringClass, factoryMethod) - .generateCode(factoryMethod.getParameterTypes()) - : NO_ARGS; + new AutowiredArgumentsCodeGenerator(ClassUtils.getUserClass(targetClass), factoryMethod) + .generateCode(factoryMethod.getParameterTypes()) : NO_ARGS; CodeBlock newInstance = generateNewInstanceCodeForMethod( - dependsOnBean, declaringClass, factoryMethodName, arguments); + factoryBeanName, ClassUtils.getUserClass(targetClass), factoryMethodName, arguments); code.add(generateWithGeneratorCode(hasArguments, newInstance)); method.addStatement(code.build()); } private CodeBlock generateInstanceSupplierForFactoryMethod(Method factoryMethod, - Class suppliedType, Class declaringClass, String factoryMethodName) { + Class suppliedType, Class targetClass, String factoryMethodName) { if (factoryMethod.getParameterCount() == 0) { return CodeBlock.of("return $T.<$T>forFactoryMethod($T.class, $S)", - BeanInstanceSupplier.class, suppliedType, declaringClass, factoryMethodName); + BeanInstanceSupplier.class, suppliedType, targetClass, factoryMethodName); } - CodeBlock parameterTypes = generateParameterTypesCode(factoryMethod.getParameterTypes(), 0); + CodeBlock parameterTypes = generateParameterTypesCode(factoryMethod.getParameterTypes()); return CodeBlock.of("return $T.<$T>forFactoryMethod($T.class, $S, $L)", - BeanInstanceSupplier.class, suppliedType, declaringClass, factoryMethodName, parameterTypes); + BeanInstanceSupplier.class, suppliedType, targetClass, factoryMethodName, parameterTypes); } - private CodeBlock generateNewInstanceCodeForMethod(boolean dependsOnBean, - Class declaringClass, String factoryMethodName, CodeBlock args) { + private CodeBlock generateNewInstanceCodeForMethod(@Nullable String factoryBeanName, + Class targetClass, String factoryMethodName, CodeBlock args) { - if (!dependsOnBean) { - return CodeBlock.of("$T.$L($L)", declaringClass, factoryMethodName, args); + if (factoryBeanName == null) { + return CodeBlock.of("$T.$L($L)", targetClass, factoryMethodName, args); } - return CodeBlock.of("$L.getBeanFactory().getBean($T.class).$L($L)", - REGISTERED_BEAN_PARAMETER_NAME, declaringClass, factoryMethodName, args); + return CodeBlock.of("$L.getBeanFactory().getBean(\"$L\", $T.class).$L($L)", + REGISTERED_BEAN_PARAMETER_NAME, factoryBeanName, targetClass, factoryMethodName, args); } private CodeBlock generateReturnStatement(GeneratedMethod generatedMethod) { @@ -374,16 +371,18 @@ private CodeBlock generateWithGeneratorCode(boolean hasArguments, CodeBlock newI return code.build(); } - private Visibility getAccessVisibility(RegisteredBean registeredBean, Member member) { - AccessControl beanTypeAccessControl = AccessControl.forResolvableType(registeredBean.getBeanType()); + private boolean isVisible(Member member, Class targetClass) { + AccessControl classAccessControl = AccessControl.forClass(targetClass); AccessControl memberAccessControl = AccessControl.forMember(member); - return AccessControl.lowest(beanTypeAccessControl, memberAccessControl).getVisibility(); - } + Visibility visibility = AccessControl.lowest(classAccessControl, memberAccessControl).getVisibility(); + return (visibility == Visibility.PUBLIC || (visibility != Visibility.PRIVATE && + member.getDeclaringClass().getPackageName().equals(this.className.packageName()))); + } - private CodeBlock generateParameterTypesCode(Class[] parameterTypes, int offset) { + private CodeBlock generateParameterTypesCode(Class[] parameterTypes) { CodeBlock.Builder code = CodeBlock.builder(); - for (int i = offset; i < parameterTypes.length; i++) { - code.add(i != offset ? ", " : ""); + for (int i = 0; i < parameterTypes.length; i++) { + code.add(i > 0 ? ", " : ""); code.add("$T.class", parameterTypes[i]); } return code.build(); @@ -395,59 +394,42 @@ private GeneratedMethod generateGetInstanceSupplierMethod(Consumer 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; } } } return false; } - } - private static class ProxyRuntimeHintsRegistrar { - - private final AutowireCandidateResolver candidateResolver; + private record ProxyRuntimeHintsRegistrar(AutowireCandidateResolver candidateResolver) { - public ProxyRuntimeHintsRegistrar(AutowireCandidateResolver candidateResolver) { - this.candidateResolver = candidateResolver; - } - - public void registerRuntimeHints(RuntimeHints runtimeHints, Method method) { - Class[] parameterTypes = method.getParameterTypes(); + public void registerRuntimeHints(RuntimeHints runtimeHints, Executable executable) { + Class[] parameterTypes = executable.getParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { - MethodParameter methodParam = new MethodParameter(method, i); + MethodParameter methodParam = MethodParameter.forExecutable(executable, i); DependencyDescriptor dependencyDescriptor = new DependencyDescriptor(methodParam, true); registerProxyIfNecessary(runtimeHints, dependencyDescriptor); } } - public void registerRuntimeHints(RuntimeHints runtimeHints, Constructor constructor) { - Class[] parameterTypes = constructor.getParameterTypes(); - for (int i = 0; i < parameterTypes.length; i++) { - MethodParameter methodParam = new MethodParameter(constructor, i); - DependencyDescriptor dependencyDescriptor = new DependencyDescriptor( - methodParam, true); - registerProxyIfNecessary(runtimeHints, dependencyDescriptor); - } - } - private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescriptor dependencyDescriptor) { Class proxyType = this.candidateResolver.getLazyResolutionProxyClass(dependencyDescriptor, null); if (proxyType != null && Proxy.isProxyClass(proxyType)) { @@ -456,4 +438,12 @@ private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescr } } + + record ConstructorDescriptor(String beanName, Constructor constructor, Class publicType) { + + Class actualType() { + return this.constructor.getDeclaringClass(); + } + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java deleted file mode 100644 index e7b715dd006c..000000000000 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/ResolvableTypeCodeGenerator.java +++ /dev/null @@ -1,69 +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.beans.factory.aot; - -import java.util.Arrays; - -import org.springframework.core.ResolvableType; -import org.springframework.javapoet.CodeBlock; -import org.springframework.util.ClassUtils; - -/** - * Internal code generator used to support {@link ResolvableType}. - * - * @author Stephane Nicoll - * @author Phillip Webb - * @since 6.0 - */ -final class ResolvableTypeCodeGenerator { - - - private ResolvableTypeCodeGenerator() { - } - - - public static CodeBlock generateCode(ResolvableType resolvableType) { - return generateCode(resolvableType, false); - } - - private static CodeBlock generateCode(ResolvableType resolvableType, boolean allowClassResult) { - if (ResolvableType.NONE.equals(resolvableType)) { - return CodeBlock.of("$T.NONE", ResolvableType.class); - } - Class type = ClassUtils.getUserClass(resolvableType.toClass()); - if (resolvableType.hasGenerics() && !resolvableType.hasUnresolvableGenerics()) { - return generateCodeWithGenerics(resolvableType, type); - } - if (allowClassResult) { - return CodeBlock.of("$T.class", type); - } - return CodeBlock.of("$T.forClass($T.class)", ResolvableType.class, type); - } - - private static CodeBlock generateCodeWithGenerics(ResolvableType target, Class type) { - ResolvableType[] generics = target.getGenerics(); - boolean hasNoNestedGenerics = Arrays.stream(generics).noneMatch(ResolvableType::hasGenerics); - CodeBlock.Builder code = CodeBlock.builder(); - code.add("$T.forClassWithGenerics($T.class", ResolvableType.class, type); - for (ResolvableType generic : generics) { - code.add(", $L", generateCode(generic, hasNoNestedGenerics)); - } - code.add(")"); - return code.build(); - } - -} 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 7715f33faa6b..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; } @@ -183,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; } @@ -207,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 @@ -233,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 efda24780820..46dff4eab47c 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 @@ -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} @@ -105,7 +106,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory { /** * Suffix for the "original instance" convention when initializing an existing * bean instance: to be appended to the fully-qualified bean class name, - * e.g. "com.mypackage.MyClass.ORIGINAL", in order to enforce the given instance + * for example, "com.mypackage.MyClass.ORIGINAL", in order to enforce the given instance * to be returned, i.e. no proxies etc. * @since 5.1 * @see #initializeBean(Object, String) @@ -128,7 +129,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory { * Constructor resolution is based on Kotlin primary / single public / single non-public, * with a fallback to the default constructor in ambiguous scenarios, also influenced * by {@link SmartInstantiationAwareBeanPostProcessor#determineCandidateConstructors} - * (e.g. for annotation-driven constructor selection). + * (for example, for annotation-driven constructor selection). * @param beanClass the class of the bean to create * @return the new bean instance * @throws BeansException if instantiation or wiring failed @@ -137,7 +138,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory { /** * Populate the given bean instance through applying after-instantiation callbacks - * and bean property post-processing (e.g. for annotation-driven injection). + * and bean property post-processing (for example, for annotation-driven injection). *

Note: This is essentially intended for (re-)populating annotated fields and * methods, either for new instances or for deserialized instances. It does * not imply traditional by-name or by-type autowiring of properties; @@ -196,7 +197,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory { * Instantiate a new bean instance of the given class with the specified autowire * strategy. All constants defined in this interface are supported here. * Can also be invoked with {@code AUTOWIRE_NO} in order to just apply - * before-instantiation callbacks (e.g. for annotation-driven injection). + * before-instantiation callbacks (for example, for annotation-driven injection). *

Does not apply standard {@link BeanPostProcessor BeanPostProcessors} * callbacks or perform any further initialization of the bean. This interface * offers distinct, fine-grained operations for those purposes, for example @@ -223,7 +224,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory { /** * Autowire the bean properties of the given bean instance by name or type. * Can also be invoked with {@code AUTOWIRE_NO} in order to just apply - * after-instantiation callbacks (e.g. for annotation-driven injection). + * after-instantiation callbacks (for example, for annotation-driven injection). *

Does not apply standard {@link BeanPostProcessor BeanPostProcessors} * callbacks or perform any further initialization of the bean. This interface * offers distinct, fine-grained operations for those purposes, for example @@ -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 5be39a0eaa11..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 @@ -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,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. @@ -151,14 +149,16 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { /** * Set the names of the beans that this bean depends on being initialized. * The bean factory will guarantee that these beans get initialized first. + *

Note that dependencies are normally expressed through bean properties or + * 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. @@ -178,6 +178,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * Set whether this bean is a primary autowire candidate. *

If this value is {@code true} for exactly one bean among multiple * matching candidates, it will serve as a tie-breaker. + * @see #setFallback */ void setPrimary(boolean primary); @@ -186,18 +187,39 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { */ boolean isPrimary(); + /** + * Set whether this bean is a fallback autowire candidate. + *

If this value is {@code true} for all beans but one among multiple + * matching candidates, the remaining bean will be selected. + * @since 6.2 + * @see #setPrimary + */ + void setFallback(boolean fallback); + + /** + * Return whether this bean is a fallback autowire candidate. + * @since 6.2 + */ + boolean isFallback(); + /** * Specify the factory bean to use, if any. - * This the name of the bean to call the specified factory method on. + * This is the name of the bean to call the specified factory method on. + *

A factory bean name is only necessary for instance-based factory methods. + * For static factory methods, the method will be derived from the bean class. * @see #setFactoryMethodName + * @see #setBeanClassName */ void setFactoryBeanName(@Nullable String factoryBeanName); /** * Return the factory bean name, if any. + *

This will be {@code null} for static factory methods which will + * be derived from the bean class instead. + * @see #getFactoryMethodName() + * @see #getBeanClassName() */ - @Nullable - String getFactoryBeanName(); + @Nullable String getFactoryBeanName(); /** * Specify a factory method, if any. This method will be invoked with @@ -211,9 +233,10 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { /** * Return a factory method, if any. + * @see #getFactoryBeanName() + * @see #getBeanClassName() */ - @Nullable - String getFactoryMethodName(); + @Nullable String getFactoryMethodName(); /** * Return the constructor argument values for this bean. @@ -225,6 +248,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { /** * Return if there are constructor argument values defined for this bean. * @since 5.0.2 + * @see #getConstructorArgumentValues() */ default boolean hasConstructorArgumentValues() { return !getConstructorArgumentValues().isEmpty(); @@ -240,6 +264,7 @@ default boolean hasConstructorArgumentValues() { /** * Return if there are property values defined for this bean. * @since 5.0.2 + * @see #getPropertyValues() */ default boolean hasPropertyValues() { return !getPropertyValues().isEmpty(); @@ -255,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. @@ -268,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 @@ -301,8 +324,7 @@ default boolean hasPropertyValues() { /** * Return a human-readable description of this bean definition. */ - @Nullable - String getDescription(); + @Nullable String getDescription(); // Read-only attributes @@ -334,7 +356,8 @@ default boolean hasPropertyValues() { boolean isPrototype(); /** - * Return whether this bean is "abstract", that is, not meant to be instantiated. + * Return whether this bean is "abstract", that is, not meant to be instantiated + * itself but rather just serving as parent for concrete child bean definitions. */ boolean isAbstract(); @@ -342,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. @@ -351,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 737746018bad..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,9 @@ package org.springframework.beans.factory.config; import java.beans.PropertyEditor; +import java.util.concurrent.Executor; + +import org.jspecify.annotations.Nullable; import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.beans.PropertyEditorRegistry; @@ -27,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; /** @@ -93,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. @@ -112,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 @@ -143,8 +143,22 @@ 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}) + * for background bootstrapping. + * @since 6.2 + * @see org.springframework.beans.factory.support.AbstractBeanDefinition#setBackgroundInit + */ + void setBootstrapExecutor(@Nullable Executor executor); + + /** + * Return the {@link Executor} (possibly a {@link org.springframework.core.task.TaskExecutor}) + * for background bootstrapping, if any. + * @since 6.2 + */ + @Nullable Executor getBootstrapExecutor(); /** * Specify a {@link ConversionService} to use for converting @@ -157,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. @@ -166,7 +179,11 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * on the given registry, fresh for each bean creation attempt. This avoids * the need for synchronization on custom editors; hence, it is generally * preferable to use this method instead of {@link #registerCustomEditor}. + *

If the given registrar implements + * {@link PropertyEditorRegistrar#overridesDefaultEditors()} to return {@code true}, + * it will be applied lazily (only when default editors are actually needed). * @param registrar the PropertyEditorRegistrar to register + * @see PropertyEditorRegistrar#overridesDefaultEditors() */ void addPropertyEditorRegistrar(PropertyEditorRegistrar registrar); @@ -224,13 +241,12 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single boolean hasEmbeddedValueResolver(); /** - * Resolve the given embedded value, e.g. an annotation attribute. + * Resolve the given embedded value, for example, an annotation attribute. * @param value the value to resolve * @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 @@ -238,7 +254,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single *

Note: Post-processors submitted here will be applied in the order of * registration; any ordering semantics expressed through implementing the * {@link org.springframework.core.Ordered} interface will be ignored. Note - * that autodetected post-processors (e.g. as beans in an ApplicationContext) + * that autodetected post-processors (for example, as beans in an ApplicationContext) * will always be applied after programmatically registered ones. * @param beanPostProcessor the post-processor to register */ @@ -273,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 249d6bc31d1b..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. @@ -66,13 +67,13 @@ public interface ConfigurableListableBeanFactory * Register a special dependency type with corresponding autowired value. *

This is intended for factory/context references that are supposed * to be autowirable but are not defined as beans in the factory: - * e.g. a dependency of type ApplicationContext resolved to the + * for example, a dependency of type ApplicationContext resolved to the * ApplicationContext instance that the bean is living in. *

Note: There are no such default types registered in a plain BeanFactory, * not even for the BeanFactory interface itself. * @param dependencyType the dependency type to register. This will typically * be a base interface such as BeanFactory, with extensions of it resolved - * as well if declared as an autowiring dependency (e.g. ListableBeanFactory), + * as well if declared as an autowiring dependency (for example, ListableBeanFactory), * as long as the given value actually implements the extended interface. * @param autowiredValue the corresponding autowired value. This may also be an * implementation of the {@link org.springframework.beans.factory.ObjectFactory} @@ -126,7 +127,7 @@ boolean isAutowireCandidate(String beanName, DependencyDescriptor descriptor) * Clear the merged bean definition cache, removing entries for beans * which are not considered eligible for full metadata caching yet. *

Typically triggered after changes to the original bean definitions, - * e.g. after applying a {@link BeanFactoryPostProcessor}. Note that metadata + * for example, after applying a {@link BeanFactoryPostProcessor}. Note that metadata * for beans which have already been created at this point will be kept around. * @since 4.2 * @see #getBeanDefinition 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 9250915504c1..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; /** @@ -76,7 +76,7 @@ * *

* Also supports "java.lang.String[]"-style array class names and primitive - * class names (e.g. "boolean"). Delegates to {@link ClassUtils} for actual + * class names (for example, "boolean"). Delegates to {@link ClassUtils} for actual * class name resolution. * *

NOTE: Custom property editors registered with this configurer do @@ -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 4825563735ff..41c8a815daab 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-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. @@ -26,6 +26,7 @@ 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; @@ -36,7 +37,6 @@ 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 +52,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 +66,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; /** @@ -148,10 +142,10 @@ public DependencyDescriptor(DependencyDescriptor original) { this.parameterTypes = original.parameterTypes; this.parameterIndex = original.parameterIndex; this.fieldName = original.fieldName; - this.containingClass = original.containingClass; this.required = original.required; this.eager = original.eager; this.nestingLevel = original.nestingLevel; + this.containingClass = original.containingClass; } @@ -169,9 +163,7 @@ 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))); + (KotlinDetector.isKotlinType(this.field.getDeclaringClass()) && KotlinDelegate.isNullable(this.field))); } else { return !obtainMethodParameter().isOptional(); @@ -180,7 +172,7 @@ public boolean isRequired() { /** * Check whether the underlying field is annotated with any variant of a - * {@code Nullable} annotation, e.g. {@code jakarta.annotation.Nullable} or + * {@code Nullable} annotation, for example, {@code jakarta.annotation.Nullable} or * {@code edu.umd.cs.findbugs.annotations.Nullable}. */ private boolean hasNullableAnnotation() { @@ -213,8 +205,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 +221,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; } @@ -332,6 +322,10 @@ public DependencyDescriptor forFallbackMatch() { public boolean fallbackMatchAllowed() { return true; } + @Override + public boolean usesStandardBeanLookup() { + return true; + } }; } @@ -351,8 +345,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()); } @@ -375,6 +368,31 @@ public Class getDependencyType() { } } + /** + * Determine whether this dependency supports lazy resolution, + * for example, through extra proxying. The default is {@code true}. + * @since 6.1.2 + * @see org.springframework.beans.factory.support.AutowireCandidateResolver#getLazyResolutionProxyIfNecessary + */ + public boolean supportsLazyResolution() { + return true; + } + + /** + * Determine whether this descriptor uses a standard bean lookup + * in {@link #resolveCandidate(String, Class, BeanFactory)} and + * therefore qualifies for factory-level shortcut resolution. + *

By default, the {@code DependencyDescriptor} class itself + * uses a standard bean lookup but subclasses may override this. + * If a subclass overrides other methods but preserves a standard + * bean lookup, it may override this method to return {@code true}. + * @since 6.2 + * @see #resolveCandidate(String, Class, BeanFactory) + */ + public boolean usesStandardBeanLookup() { + return (getClass() == DependencyDescriptor.class); + } + @Override public boolean equals(@Nullable Object other) { @@ -384,9 +402,9 @@ public boolean equals(@Nullable Object other) { if (!super.equals(other)) { return false; } - DependencyDescriptor otherDesc = (DependencyDescriptor) other; - return (this.required == otherDesc.required && this.eager == otherDesc.eager && - this.nestingLevel == otherDesc.nestingLevel && this.containingClass == otherDesc.containingClass); + return (other instanceof DependencyDescriptor otherDesc && this.required == otherDesc.required && + this.eager == otherDesc.eager && this.nestingLevel == otherDesc.nestingLevel && + this.containingClass == otherDesc.containingClass); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DeprecatedBeanWarner.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DeprecatedBeanWarner.java index 6a1369af0f7a..ee05ce45d52a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DeprecatedBeanWarner.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DeprecatedBeanWarner.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. @@ -85,7 +85,7 @@ protected void logDeprecatedBean(String beanName, Class beanType, BeanDefinit builder.append(beanName); builder.append('\''); String resourceDescription = beanDefinition.getResourceDescription(); - if (StringUtils.hasLength(resourceDescription)) { + if (StringUtils.hasText(resourceDescription)) { builder.append(" in "); builder.append(resourceDescription); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareBeanPostProcessor.java index dd1c542a689b..a8ba25030aff 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareBeanPostProcessor.java @@ -31,7 +31,7 @@ public interface DestructionAwareBeanPostProcessor extends BeanPostProcessor { /** * Apply this BeanPostProcessor to the given bean instance before its - * destruction, e.g. invoking custom destruction callbacks. + * destruction, for example, invoking custom destruction callbacks. *

Like DisposableBean's {@code destroy} and a custom destroy method, this * callback will only apply to beans which the container fully manages the * lifecycle for. This is usually the case for singletons and scoped beans. 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 9f7657f196c4..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,14 +125,13 @@ 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; } /** * Set a fully qualified static field name to retrieve, - * e.g. "example.MyExampleClass.MY_EXAMPLE_FIELD". + * for example, "example.MyExampleClass.MY_EXAMPLE_FIELD". * Convenient alternative to specifying targetClass and targetField. * @see #setTargetClass * @see #setTargetField @@ -167,6 +158,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Override + @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"); @@ -189,7 +181,7 @@ public void afterPropertiesSet() throws ClassNotFoundException, NoSuchFieldExcep if (lastDotIndex == -1 || lastDotIndex == this.staticField.length()) { throw new IllegalArgumentException( "staticField must be a fully qualified class plus static field name: " + - "e.g. 'example.MyExampleClass.MY_EXAMPLE_FIELD'"); + "for example, 'example.MyExampleClass.MY_EXAMPLE_FIELD'"); } String className = this.staticField.substring(0, lastDotIndex); String fieldName = this.staticField.substring(lastDotIndex + 1); @@ -209,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(); } @@ -226,7 +217,7 @@ public Object getObject() throws IllegalAccessException { } @Override - 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 3ff39d81e5e3..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,7 +135,7 @@ public Object getObject() throws Exception { * or {@code null} if not known in advance. */ @Override - 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 91910a2f4a24..6380f164bbe1 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-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,11 +16,12 @@ 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.util.StringValueResolver; /** @@ -100,6 +101,8 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi /** Default value separator: {@value}. */ public static final String DEFAULT_VALUE_SEPARATOR = ":"; + /** Default escape character: {@code '\'}. */ + public static final Character DEFAULT_ESCAPE_CHARACTER = '\\'; /** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */ protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX; @@ -108,21 +111,20 @@ 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}. */ + protected @Nullable Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER; 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; /** @@ -151,6 +153,17 @@ 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}. + * @since 6.2 + */ + public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) { + this.escapeCharacter = escsEscapeCharacter; + } + /** * Specify whether to trim resolved values before applying them, * removing superfluous whitespace from the beginning and end. @@ -163,7 +176,7 @@ public void setTrimValues(boolean trimValues) { /** * Set a value that should be treated as {@code null} when resolved - * as a placeholder value: e.g. "" (empty String) or "null". + * as a placeholder value: for example, "" (empty String) or "null". *

Note that this will only apply to full property values, * not to parts of concatenated values. *

By default, no such null value is defined. This means that @@ -211,7 +224,6 @@ public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } - 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..e100e9e31410 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 @@ -20,9 +20,10 @@ 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 @@ -47,11 +48,9 @@ @Deprecated 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 +119,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 e073d81b47db..840a34e76234 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-2018 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.beans.factory.config; -import java.util.Collections; import java.util.Enumeration; import java.util.Properties; import java.util.Set; @@ -77,7 +76,7 @@ public class PropertyOverrideConfigurer extends PropertyResourceConfigurer { /** * Contains names of beans that have overrides. */ - private final Set beanNames = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + private final Set beanNames = ConcurrentHashMap.newKeySet(16); /** 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 b86e633f744f..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; /** @@ -121,7 +117,7 @@ public void setTargetObject(Object targetObject) { * Specify the name of a target bean to apply the property path to. * Alternatively, specify a target object directly. * @param targetBeanName the bean name to be looked up in the - * containing bean factory (e.g. "testBean") + * containing bean factory (for example, "testBean") * @see #setTargetObject */ public void setTargetBeanName(String targetBeanName) { @@ -131,7 +127,7 @@ public void setTargetBeanName(String targetBeanName) { /** * Specify the property path to apply to the target. * @param propertyPath the property path, potentially nested - * (e.g. "age" or "spouse.age") + * (for example, "age" or "spouse.age") */ public void setPropertyPath(String propertyPath) { this.propertyPath = StringUtils.trimAllWhitespace(propertyPath); @@ -201,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 && @@ -224,7 +219,7 @@ public Object getObject() throws BeansException { } @Override - 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 0fba4f79c229..77d430073e67 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-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,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; @@ -93,7 +94,7 @@ public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport /** * Set the system property mode by the name of the corresponding constant, - * e.g. "SYSTEM_PROPERTIES_MODE_OVERRIDE". + * for example, "SYSTEM_PROPERTIES_MODE_OVERRIDE". * @param constantName name of the constant * @see #setSystemPropertiesMode */ @@ -153,8 +154,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 +181,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 +194,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) { @@ -234,13 +232,13 @@ private class PlaceholderResolvingStringValueResolver implements StringValueReso public PlaceholderResolvingStringValueResolver(Properties props) { this.helper = new PropertyPlaceholderHelper( - placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders); + placeholderPrefix, placeholderSuffix, valueSeparator, + escapeCharacter, ignoreUnresolvablePlaceholders); this.resolver = new PropertyPlaceholderConfigurerResolver(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(); @@ -259,8 +257,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 9606eb87264b..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}, @@ -31,7 +32,7 @@ *

{@link org.springframework.context.ApplicationContext} implementations * such as a {@link org.springframework.web.context.WebApplicationContext} * may register additional standard scopes specific to their environment, - * e.g. {@link org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST "request"} + * for example, {@link org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST "request"} * and {@link org.springframework.web.context.WebApplicationContext#SCOPE_SESSION "session"}, * based on this Scope SPI. * @@ -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 @@ -125,13 +125,12 @@ public interface Scope { /** * Resolve the contextual object for the given key, if any. - * E.g. the HttpServletRequest object for key "request". + * For example, the HttpServletRequest object for key "request". * @param key the contextual key * @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 8d0cf7dd397e..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,13 +325,12 @@ else if (paramTypes[i].isInstance(cause)) { @Override - @Nullable - public Object getObject() { + public @Nullable Object getObject() { return this.proxy; } @Override - public Class getObjectType() { + public @Nullable Class getObjectType() { return this.serviceLocatorInterface; } @@ -393,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 756cc1cce2b9..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 @@ -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,13 +16,14 @@ package org.springframework.beans.factory.config; -import java.util.LinkedHashSet; 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; /** * Simple factory for shared Set instances. Allows for central setup @@ -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; /** @@ -85,7 +84,7 @@ protected Set createInstance() { result = BeanUtils.instantiateClass(this.targetSetClass); } else { - result = new LinkedHashSet<>(this.sourceSet.size()); + result = CollectionUtils.newLinkedHashSet(this.sourceSet.size()); } Class valueType = null; if (this.targetSetClass != null) { 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 ee6d4a778cac..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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,9 @@ package org.springframework.beans.factory.config; -import org.springframework.lang.Nullable; +import java.util.function.Consumer; + +import org.jspecify.annotations.Nullable; /** * Interface that defines a registry for shared bean instances. @@ -57,6 +59,17 @@ public interface SingletonBeanRegistry { */ void registerSingleton(String beanName, Object singletonObject); + /** + * Add a callback to be triggered when the specified singleton becomes available + * in the bean registry. + * @param beanName the name of the bean + * @param singletonConsumer a callback for reacting to the availability of the freshly + * registered/created singleton instance (intended for follow-up steps before the bean is + * actively used by other callers, not for modifying the given singleton instance itself) + * @since 6.2 + */ + void addSingletonCallback(String beanName, Consumer singletonConsumer); + /** * Return the (raw) singleton object registered under the given name. *

Only checks already instantiated singletons; does not return an Object @@ -70,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. @@ -129,7 +141,10 @@ public interface SingletonBeanRegistry { * Return the singleton mutex used by this registry (for external collaborators). * @return the mutex object (never {@code null}) * @since 4.2 + * @deprecated as of 6.2, in favor of lenient singleton locking + * (with this method returning an arbitrary object to lock on) */ + @Deprecated(since = "6.2") Object getSingletonMutex(); } 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 cd54203aea54..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 @@ -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,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; @@ -87,20 +86,20 @@ default Constructor[] determineCandidateConstructors(Class beanClass, Stri * typically for the purpose of resolving a circular reference. *

This callback gives post-processors a chance to expose a wrapper * early - that is, before the target bean instance is fully initialized. - * The exposed object should be equivalent to the what + * The exposed object should be equivalent to what * {@link #postProcessBeforeInitialization} / {@link #postProcessAfterInitialization} * would expose otherwise. Note that the object returned by this method will - * be used as bean reference unless the post-processor returns a different - * wrapper from said post-process callbacks. In other words: Those post-process + * be used as the bean reference unless the post-processor returns a different + * wrapper from said post-process callbacks. In other words, those post-process * callbacks may either eventually expose the same reference or alternatively * return the raw bean instance from those subsequent callbacks (if the wrapper * for the affected bean has been built for a call to this method already, - * it will be exposes as final bean reference by default). + * it will be exposed as the final bean reference by default). *

The default implementation returns the given {@code bean} as-is. * @param bean the raw bean instance * @param beanName the name of the bean - * @return the object to expose as bean reference - * (typically with the passed-in bean instance as default) + * @return the object to expose as the bean reference + * (typically the passed-in bean instance as default) * @throws org.springframework.beans.BeansException in case of errors */ default Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { 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 1b1fae321279..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; @@ -77,7 +77,7 @@ public abstract class YamlProcessor { * A map of document matchers allowing callers to selectively use only * some of the documents in a YAML resource. In YAML documents are * separated by {@code ---} lines, and each document is converted - * to properties before the match is made. E.g. + * to properties before the match is made. For example, *

 	 * environment: dev
 	 * url: https://dev.bar.com
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 0c70f097d7c0..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,
@@ -32,7 +33,7 @@
  * has a lot of similar features.
  *
  * 

Note: All exposed values are of type {@code String} for access through - * the common {@link Properties#getProperty} method (e.g. in configuration property + * the common {@link Properties#getProperty} method (for example, in configuration property * resolution through {@link PropertyResourceConfigurer#setProperties(Properties)}). * If this is not desirable, use {@link YamlMapFactoryBean} instead. * @@ -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 363ec5289621..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 @@ -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,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,7 @@ 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; @@ -150,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; /** @@ -206,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; } @@ -250,7 +248,7 @@ public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefin @SuppressWarnings("serial") Closure beans = new Closure<>(this) { @Override - public Object call(Object... args) { + public @Nullable Object call(Object... args) { invokeBeanDefiningClosure((Closure) args[0]); return null; } @@ -425,6 +423,7 @@ else if (args.length > 1 && args[args.length -1] instanceof Closure) { private boolean addDeferredProperty(String property, Object newValue) { if (newValue instanceof List || newValue instanceof Map) { + Assert.state(this.currentBeanDefinition != null, "No current bean definition set"); this.deferredProperties.put(this.currentBeanDefinition.getBeanName() + '.' + property, new DeferredProperty(this.currentBeanDefinition, property, newValue)); return true; @@ -640,6 +639,7 @@ else if (value instanceof Closure callable) { this.currentBeanDefinition = current; } } + Assert.state(this.currentBeanDefinition != null, "No current bean definition set"); this.currentBeanDefinition.addProperty(name, value); } @@ -654,7 +654,7 @@ else if (value instanceof Closure callable) { * */ @Override - public Object getProperty(String name) { + public @Nullable Object getProperty(String name) { Binding binding = getBinding(); if (binding != null && binding.hasVariable(name)) { return binding.getVariable(name); @@ -696,6 +696,7 @@ else if (this.currentBeanDefinition != null) { } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private GroovyDynamicElementReader createDynamicElementReader(String namespace) { XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext( new DescriptiveResource("Groovy")); @@ -727,9 +728,9 @@ private static class DeferredProperty { private final String name; - public Object value; + public @Nullable Object value; - public DeferredProperty(GroovyBeanDefinitionWrapper beanDefinition, String name, Object value) { + public DeferredProperty(GroovyBeanDefinitionWrapper beanDefinition, String name, @Nullable Object value) { this.beanDefinition = beanDefinition; this.name = name; this.value = value; @@ -762,20 +763,17 @@ public MetaClass getMetaClass() { } @Override - public Object getProperty(String property) { + public @Nullable Object getProperty(String property) { if (property.equals("beanName")) { return getBeanName(); } else if (property.equals("source")) { return getSource(); } - else if (this.beanDefinition != null) { + else { return new GroovyPropertyValue( property, this.beanDefinition.getBeanDefinition().getPropertyValues().get(property)); } - else { - return this.metaClass.getProperty(this, property); - } } @Override @@ -804,9 +802,9 @@ private class GroovyPropertyValue extends GroovyObjectSupport { private final String propertyName; - private final Object propertyValue; + private final @Nullable Object propertyValue; - public GroovyPropertyValue(String propertyName, Object propertyValue) { + public GroovyPropertyValue(String propertyName, @Nullable Object propertyValue) { this.propertyName = propertyName; this.propertyValue = propertyValue; } 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 e52922da0368..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 @@ -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,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) { @@ -84,15 +78,14 @@ class GroovyBeanDefinitionWrapper extends GroovyObjectSupport { this(beanName, clazz, null); } - GroovyBeanDefinitionWrapper(@Nullable String beanName, Class clazz, @Nullable Collection constructorArgs) { + GroovyBeanDefinitionWrapper(@Nullable String beanName, @Nullable Class clazz, @Nullable Collection constructorArgs) { this.beanName = beanName; this.clazz = clazz; this.constructorArgs = constructorArgs; } - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } @@ -130,11 +123,12 @@ void setBeanDefinitionHolder(BeanDefinitionHolder holder) { } BeanDefinitionHolder getBeanDefinitionHolder() { - return new BeanDefinitionHolder(getBeanDefinition(), getBeanName()); + Assert.state(this.beanName != null, "Bean name must be set"); + return new BeanDefinitionHolder(getBeanDefinition(), this.beanName); } - void setParent(Object obj) { - Assert.notNull(obj, "Parent bean cannot be set to a null runtime bean reference."); + void setParent(@Nullable Object obj) { + Assert.notNull(obj, "Parent bean cannot be set to a null runtime bean reference"); if (obj instanceof String name) { this.parentName = name; } @@ -148,7 +142,7 @@ else if (obj instanceof GroovyBeanDefinitionWrapper wrapper) { getBeanDefinition().setAbstract(false); } - GroovyBeanDefinitionWrapper addProperty(String propertyName, Object propertyValue) { + GroovyBeanDefinitionWrapper addProperty(String propertyName, @Nullable Object propertyValue) { if (propertyValue instanceof GroovyBeanDefinitionWrapper wrapper) { propertyValue = wrapper.getBeanDefinition(); } @@ -158,7 +152,7 @@ GroovyBeanDefinitionWrapper addProperty(String propertyName, Object propertyValu @Override - 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); @@ -170,7 +164,7 @@ else if (dynamicProperties.contains(property)) { } @Override - public void setProperty(String property, Object newValue) { + public void setProperty(String property, @Nullable Object newValue) { if (PARENT.equals(property)) { setParent(newValue); } 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/ComponentDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ComponentDefinition.java index 33ec279f9c24..692a5f515fb7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ComponentDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ComponentDefinition.java @@ -28,7 +28,7 @@ * it is now possible for a single logical configuration entity, in this case an XML tag, to * create multiple {@link BeanDefinition BeanDefinitions} and {@link BeanReference RuntimeBeanReferences} * in order to provide more succinct configuration and greater convenience to end users. As such, it can - * no longer be assumed that each configuration entity (e.g. XML tag) maps to one {@link BeanDefinition}. + * no longer be assumed that each configuration entity (for example, XML tag) maps to one {@link BeanDefinition}. * For tool vendors and other users who wish to present visualization or support for configuring Spring * applications it is important that there is some mechanism in place to tie the {@link BeanDefinition BeanDefinitions} * in the {@link org.springframework.beans.factory.BeanFactory} back to the configuration data in a way 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 0277bc0a0f9c..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -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(); } @@ -98,9 +97,7 @@ public String toString() { for (ParseState.Entry entry : this.state) { if (i > 0) { sb.append('\n'); - for (int j = 0; j < i; j++) { - sb.append('\t'); - } + sb.append("\t".repeat(i)); sb.append("-> "); } sb.append(entry); 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 279f96ef59a8..0fcaacd6c1bc 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 @@ -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. @@ -34,11 +34,13 @@ 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; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.BeansException; +import org.springframework.beans.InvalidPropertyException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyAccessorUtils; import org.springframework.beans.PropertyValue; @@ -72,8 +74,8 @@ 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; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodCallback; @@ -92,10 +94,10 @@ * Supports autowiring constructors, properties by name, and properties by type. * *

The main template method to be implemented by subclasses is - * {@link #resolveDependency(DependencyDescriptor, String, Set, TypeConverter)}, - * used for autowiring by type. In case of a factory which is capable of searching - * its bean definitions, matching beans will typically be implemented through such - * a search. For other factory styles, simplified matching algorithms can be implemented. + * {@link #resolveDependency(DependencyDescriptor, String, Set, TypeConverter)}, used for + * autowiring. In case of a {@link org.springframework.beans.factory.ListableBeanFactory} + * which is capable of searching its bean definitions, matching beans will typically be + * implemented through such a search. Otherwise, simplified matching can be implemented. * *

Note that this class does not assume or implement bean definition * registry capabilities. See {@link DefaultListableBeanFactory} for an implementation @@ -123,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; @@ -203,7 +204,7 @@ public InstantiationStrategy getInstantiationStrategy() { /** * Set the ParameterNameDiscoverer to use for resolving method parameter - * names if needed (e.g. for constructor names). + * names if needed (for example, for constructor names). *

Default is a {@link DefaultParameterNameDiscoverer}. */ public void setParameterNameDiscoverer(@Nullable ParameterNameDiscoverer parameterNameDiscoverer) { @@ -214,8 +215,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; } @@ -465,8 +465,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); } @@ -481,7 +480,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()) { @@ -496,15 +495,13 @@ protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable O if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { mbdToUse = new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); - } - - // Prepare method overrides. - try { - mbdToUse.prepareMethodOverrides(); - } - catch (BeanDefinitionValidationException ex) { - throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), - beanName, "Validation of method overrides failed", ex); + try { + mbdToUse.prepareMethodOverrides(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } } try { @@ -539,7 +536,7 @@ protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable O /** * Actually create the specified bean. Pre-creation processing has already happened - * at this point, e.g. checking {@code postProcessBeforeInstantiation} callbacks. + * at this point, for example, checking {@code postProcessBeforeInstantiation} callbacks. *

Differentiates between default bean instantiation, use of a * factory method, and autowiring a constructor. * @param beanName the name of the bean @@ -551,7 +548,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. @@ -617,7 +614,7 @@ protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); - Set actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); + Set actualDependentBeans = CollectionUtils.newLinkedHashSet(dependentBeans.length); for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); @@ -649,13 +646,12 @@ 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. if (targetType != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { - boolean matchingOnlyFactoryBean = typesToMatch.length == 1 && typesToMatch[0] == FactoryBean.class; + boolean matchingOnlyFactoryBean = (typesToMatch.length == 1 && typesToMatch[0] == FactoryBean.class); for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { Class predicted = bp.predictBeanType(targetType, beanName); if (predicted != null && @@ -675,8 +671,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 +704,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,15 +753,15 @@ 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) { paramNames = pnd.getParameterNames(candidate); } } - Set usedValueHolders = new HashSet<>(paramTypes.length); - Object[] args = new Object[paramTypes.length]; + Set usedValueHolders = CollectionUtils.newHashSet(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); @@ -814,30 +808,48 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m // Common return type found: all factory methods return same type. For a non-parameterized // unique candidate, cache the full type declaration context of the target factory method. - cachedReturnType = (uniqueCandidate != null ? - ResolvableType.forMethodReturnType(uniqueCandidate) : ResolvableType.forClass(commonType)); - mbd.factoryMethodReturnType = cachedReturnType; - return cachedReturnType.resolve(); + try { + cachedReturnType = (uniqueCandidate != null ? + ResolvableType.forMethodReturnType(uniqueCandidate) : ResolvableType.forClass(commonType)); + mbd.factoryMethodReturnType = cachedReturnType; + return cachedReturnType.resolve(); + } + catch (LinkageError err) { + // For example, a NoClassDefFoundError for a generic method return type + if (logger.isDebugEnabled()) { + logger.debug("Failed to resolve type for factory method of bean '" + beanName + "': " + + (uniqueCandidate != null ? uniqueCandidate : commonType), err); + } + return null; + } } /** * This implementation attempts to query the FactoryBean's generic parameter metadata * if present to determine the object type. If not present, i.e. the FactoryBean is - * declared as a raw type, checks the FactoryBean's {@code getObjectType} method + * declared as a raw type, it checks the FactoryBean's {@code getObjectType} method * on a plain instance of the FactoryBean, without bean properties applied yet. - * If this doesn't return a type yet, and {@code allowInit} is {@code true} a - * full creation of the FactoryBean is used as fallback (through delegation to the - * superclass's implementation). + * If this doesn't return a type yet and {@code allowInit} is {@code true}, full + * creation of the FactoryBean is attempted as fallback (through delegation to the + * superclass implementation). *

The shortcut check for a FactoryBean is only applied in case of a singleton * FactoryBean. If the FactoryBean instance itself is not kept as singleton, * it will be fully created to check the type of its exposed object. */ @Override protected ResolvableType getTypeForFactoryBean(String beanName, RootBeanDefinition mbd, boolean allowInit) { + ResolvableType result; + // Check if the bean definition itself has defined the type with an attribute - ResolvableType result = getTypeForFactoryBeanFromAttributes(mbd); - if (result != ResolvableType.NONE) { - return result; + try { + result = getTypeForFactoryBeanFromAttributes(mbd); + if (result != ResolvableType.NONE) { + return result; + } + } + catch (IllegalArgumentException ex) { + throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName, + String.valueOf(ex.getMessage())); } // For instance supplied beans, try the target type and bean class immediately @@ -971,9 +983,13 @@ 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) { - synchronized (getSingletonMutex()) { + private @Nullable FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) { + boolean locked = this.singletonLock.tryLock(); + if (!locked) { + return null; + } + + try { BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName); if (bw != null) { return (FactoryBean) bw.getWrappedInstance(); @@ -996,6 +1012,7 @@ private FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, Root if (instance == null) { bw = createBeanInstance(beanName, mbd, null); instance = bw.getWrappedInstance(); + this.factoryBeanInstanceCache.put(beanName, bw); } } catch (UnsatisfiedDependencyException ex) { @@ -1020,11 +1037,10 @@ private FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, Root afterSingletonCreation(beanName); } - FactoryBean fb = getFactoryBean(beanName, instance); - if (bw != null) { - this.factoryBeanInstanceCache.put(beanName, bw); - } - return fb; + return getFactoryBean(beanName, instance); + } + finally { + this.singletonLock.unlock(); } } @@ -1036,8 +1052,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; } @@ -1095,8 +1110,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. @@ -1125,8 +1139,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) { @@ -1148,7 +1161,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); @@ -1157,9 +1170,11 @@ protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd "Bean class isn't public, and non-public access not allowed: " + beanClass.getName()); } - Supplier instanceSupplier = mbd.getInstanceSupplier(); - if (instanceSupplier != null) { - return obtainFromSupplier(instanceSupplier, beanName, mbd); + if (args == null) { + Supplier instanceSupplier = mbd.getInstanceSupplier(); + if (instanceSupplier != null) { + return obtainFromSupplier(instanceSupplier, beanName, mbd); + } } if (mbd.getFactoryMethodName() != null) { @@ -1248,8 +1263,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) { @@ -1286,8 +1300,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()) { @@ -1331,7 +1344,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); } @@ -1351,7 +1364,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); } @@ -1685,8 +1698,7 @@ protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrap } Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); Object convertedValue = resolvedValue; - boolean convertible = bw.isWritableProperty(propertyName) && - !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); + boolean convertible = isConvertibleProperty(propertyName, bw); if (convertible) { convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); } @@ -1723,11 +1735,23 @@ else if (convertible && originalValue instanceof TypedStringValue typedStringVal } } + /** + * Determine whether the factory should cache a converted value for the given property. + */ + private boolean isConvertibleProperty(String propertyName, BeanWrapper bw) { + try { + return !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName) && + BeanUtils.hasUniqueWriteMethod(bw.getPropertyDescriptor(propertyName)); + } + catch (InvalidPropertyException ex) { + return false; + } + } + /** * 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) { @@ -1872,7 +1896,7 @@ protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefi if (logger.isTraceEnabled()) { logger.trace("Invoking init method '" + methodName + "' on bean with name '" + beanName + "'"); } - Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod, beanClass); + Method methodToInvoke = ClassUtils.getPubliclyAccessibleMethodIfPossible(initMethod, beanClass); try { ReflectionUtils.makeAccessible(methodToInvoke); @@ -1901,10 +1925,8 @@ protected Object postProcessObjectFromFactoryBean(Object object, String beanName */ @Override protected void removeSingleton(String beanName) { - synchronized (getSingletonMutex()) { - super.removeSingleton(beanName); - this.factoryBeanInstanceCache.remove(beanName); - } + super.removeSingleton(beanName); + this.factoryBeanInstanceCache.remove(beanName); } /** @@ -1912,10 +1934,8 @@ protected void removeSingleton(String beanName) { */ @Override protected void clearSingletonCache() { - synchronized (getSingletonMutex()) { - super.clearSingletonCache(); - this.factoryBeanInstanceCache.clear(); - } + super.clearSingletonCache(); + this.factoryBeanInstanceCache.clear(); } /** @@ -1945,8 +1965,7 @@ public CreateFromClassBeanDefinition(CreateFromClassBeanDefinition original) { } @Override - @Nullable - public Constructor[] getPreferredConstructors() { + public Constructor @Nullable [] getPreferredConstructors() { Constructor[] fromAttribute = super.getPreferredConstructors(); if (fromAttribute != null) { return fromAttribute; @@ -1973,8 +1992,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 d8b4f3a41a87..2a8a44e3268f 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-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. @@ -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; @@ -51,6 +52,7 @@ * @author Juergen Hoeller * @author Rob Harrop * @author Mark Fisher + * @author Sebastien Deleuze * @see GenericBeanDefinition * @see RootBeanDefinition * @see ChildBeanDefinition @@ -139,6 +141,18 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess */ public static final String PREFERRED_CONSTRUCTORS_ATTRIBUTE = "preferredConstructors"; + /** + * The name of an attribute that can be + * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a + * {@link org.springframework.beans.factory.config.BeanDefinition} so that + * bean definitions can indicate the sort order for the targeted bean. + * This is analogous to the {@code @Order} annotation. + * @since 6.1.2 + * @see org.springframework.core.annotation.Order + * @see org.springframework.core.Ordered + */ + public static final String ORDER_ATTRIBUTE = "order"; + /** * Constant that indicates the container should attempt to infer the * {@link #setDestroyMethodName destroy method name} for a bean as opposed to @@ -152,56 +166,51 @@ 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; - @Nullable - private Boolean lazyInit; + private boolean backgroundInit = false; + + 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; + private boolean defaultCandidate = true; + private boolean primary = false; + private boolean fallback = false; + 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; @@ -211,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; /** @@ -263,6 +270,7 @@ protected AbstractBeanDefinition(BeanDefinition original) { if (originalAbd.hasMethodOverrides()) { setMethodOverrides(new MethodOverrides(originalAbd.getMethodOverrides())); } + setBackgroundInit(originalAbd.isBackgroundInit()); Boolean lazyInit = originalAbd.getLazyInit(); if (lazyInit != null) { setLazyInit(lazyInit); @@ -271,7 +279,9 @@ protected AbstractBeanDefinition(BeanDefinition original) { setDependencyCheck(originalAbd.getDependencyCheck()); setDependsOn(originalAbd.getDependsOn()); setAutowireCandidate(originalAbd.isAutowireCandidate()); + setDefaultCandidate(originalAbd.isDefaultCandidate()); setPrimary(originalAbd.isPrimary()); + setFallback(originalAbd.isFallback()); copyQualifiersFrom(originalAbd); setInstanceSupplier(originalAbd.getInstanceSupplier()); setNonPublicAccessAllowed(originalAbd.isNonPublicAccessAllowed()); @@ -339,6 +349,7 @@ public void overrideFrom(BeanDefinition other) { if (otherAbd.hasMethodOverrides()) { getMethodOverrides().addOverrides(otherAbd.getMethodOverrides()); } + setBackgroundInit(otherAbd.isBackgroundInit()); Boolean lazyInit = otherAbd.getLazyInit(); if (lazyInit != null) { setLazyInit(lazyInit); @@ -347,7 +358,9 @@ public void overrideFrom(BeanDefinition other) { setDependencyCheck(otherAbd.getDependencyCheck()); setDependsOn(otherAbd.getDependsOn()); setAutowireCandidate(otherAbd.isAutowireCandidate()); + setDefaultCandidate(otherAbd.isDefaultCandidate()); setPrimary(otherAbd.isPrimary()); + setFallback(otherAbd.isFallback()); copyQualifiersFrom(otherAbd); setInstanceSupplier(otherAbd.getInstanceSupplier()); setNonPublicAccessAllowed(otherAbd.isNonPublicAccessAllowed()); @@ -391,7 +404,8 @@ public void applyDefaults(BeanDefinitionDefaults defaults) { /** - * Specify the bean class name of this bean definition. + * {@inheritDoc} + * @see #setBeanClass(Class) */ @Override public void setBeanClassName(@Nullable String beanClassName) { @@ -399,11 +413,11 @@ public void setBeanClassName(@Nullable String beanClassName) { } /** - * Return the current bean class name of this bean definition. + * {@inheritDoc} + * @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); } @@ -467,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; @@ -479,9 +492,8 @@ public Class resolveBeanClass(@Nullable ClassLoader classLoader) throws Class } /** - * Return a resolvable type for this bean definition. + * {@inheritDoc} *

This implementation delegates to {@link #getBeanClass()}. - * @since 5.2 */ @Override public ResolvableType getResolvableType() { @@ -489,7 +501,7 @@ public ResolvableType getResolvableType() { } /** - * Set the name of the target scope for the bean. + * {@inheritDoc} *

The default is singleton status, although this is only applied once * a bean definition becomes active in the containing factory. A bean * definition may eventually inherit its scope from a parent bean definition. @@ -504,18 +516,17 @@ public void setScope(@Nullable String scope) { } /** - * Return the name of the target scope for the bean. + * {@inheritDoc} + *

The default is {@link #SCOPE_DEFAULT}. */ @Override - @Nullable - public String getScope() { + public @Nullable String getScope() { return this.scope; } /** - * Return whether this a Singleton, with a single shared instance - * returned from all calls. - * @see #SCOPE_SINGLETON + * {@inheritDoc} + *

The default is {@code true}. */ @Override public boolean isSingleton() { @@ -523,9 +534,8 @@ public boolean isSingleton() { } /** - * Return whether this a Prototype, with an independent instance - * returned for each call. - * @see #SCOPE_PROTOTYPE + * {@inheritDoc} + *

The default is {@code false}. */ @Override public boolean isPrototype() { @@ -535,16 +545,16 @@ public boolean isPrototype() { /** * Set if this bean is "abstract", i.e. not meant to be instantiated itself but * rather just serving as parent for concrete child bean definitions. - *

Default is "false". Specify true to tell the bean factory to not try to - * instantiate that particular bean in any case. + *

The default is "false". Specify {@code true} to tell the bean factory to + * not try to instantiate that particular bean in any case. */ public void setAbstract(boolean abstractFlag) { this.abstractFlag = abstractFlag; } /** - * Return whether this bean is "abstract", i.e. not meant to be instantiated - * itself but rather just serving as parent for concrete child bean definitions. + * {@inheritDoc} + *

The default is {@code false}. */ @Override public boolean isAbstract() { @@ -552,9 +562,39 @@ public boolean isAbstract() { } /** - * Set whether this bean should be lazily initialized. - *

If {@code false}, the bean will get instantiated on startup by bean - * factories that perform eager initialization of singletons. + * Specify the bootstrap mode for this bean: default is {@code false} for using + * the main pre-instantiation thread for non-lazy singleton beans and the caller + * thread for prototype beans. + *

Set this flag to {@code true} to allow for instantiating this bean on a + * background thread. For a non-lazy singleton, a background pre-instantiation + * thread can be used then, while still enforcing the completion at the end of + * {@link DefaultListableBeanFactory#preInstantiateSingletons()}. + * For a lazy singleton, a background pre-instantiation thread can be used as well + * - with completion allowed at a later point, enforcing it when actually accessed. + *

Note that this flag may be ignored by bean factories not set up for + * background bootstrapping, always applying single-threaded bootstrapping + * for non-lazy singleton beans. + * @since 6.2 + * @see #setLazyInit + * @see DefaultListableBeanFactory#setBootstrapExecutor + */ + public void setBackgroundInit(boolean backgroundInit) { + this.backgroundInit = backgroundInit; + } + + /** + * Return the bootstrap mode for this bean: default is {@code false} for using + * the main pre-instantiation thread for non-lazy singleton beans and the caller + * thread for prototype beans. + * @since 6.2 + */ + public boolean isBackgroundInit() { + return this.backgroundInit; + } + + /** + * {@inheritDoc} + *

The default is {@code false}. */ @Override public void setLazyInit(boolean lazyInit) { @@ -562,13 +602,12 @@ public void setLazyInit(boolean lazyInit) { } /** - * Return whether this bean should be lazily initialized, i.e. not - * eagerly instantiated on startup. Only applicable to a singleton bean. - * @return whether to apply lazy-init semantics ({@code false} by default) + * {@inheritDoc} + *

The default is {@code false}. */ @Override public boolean isLazyInit() { - return (this.lazyInit != null && this.lazyInit.booleanValue()); + return (this.lazyInit != null && this.lazyInit); } /** @@ -577,14 +616,13 @@ 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; } /** * Set the autowire mode. This determines whether any automagical detection - * and setting of bean references will happen. Default is AUTOWIRE_NO + * and setting of bean references will happen. The default is AUTOWIRE_NO * which means there won't be convention-based autowiring by name or type * (however, there may still be explicit annotation-driven autowiring). * @param autowireMode the autowire mode to set. @@ -652,32 +690,27 @@ public int getDependencyCheck() { } /** - * Set the names of the beans that this bean depends on being initialized. - * The bean factory will guarantee that these beans get initialized first. - *

Note that dependencies are normally expressed through bean properties or - * constructor arguments. This property should just be necessary for other kinds - * of dependencies like statics (*ugh*) or database preparation on startup. + * {@inheritDoc} + *

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; } /** - * Return the bean names that this bean depends on. + * {@inheritDoc} + *

The default is no beans to explicitly depend on. */ @Override - @Nullable - public String[] getDependsOn() { + public String @Nullable [] getDependsOn() { return this.dependsOn; } /** - * Set whether this bean is a candidate for getting autowired into some other bean. - *

Note that this flag is designed to only affect type-based autowiring. - * It does not affect explicit references by name, which will get resolved even - * if the specified bean is not marked as an autowire candidate. As a consequence, - * autowiring by name will nevertheless inject a bean if the name matches. + * {@inheritDoc} + *

The default is {@code true}, allowing injection by type at any injection point. + * Switch this to {@code false} in order to disable autowiring by type for this bean. * @see #AUTOWIRE_BY_TYPE * @see #AUTOWIRE_BY_NAME */ @@ -687,7 +720,8 @@ public void setAutowireCandidate(boolean autowireCandidate) { } /** - * Return whether this bean is a candidate for getting autowired into some other bean. + * {@inheritDoc} + *

The default is {@code true}. */ @Override public boolean isAutowireCandidate() { @@ -695,9 +729,32 @@ public boolean isAutowireCandidate() { } /** - * Set whether this bean is a primary autowire candidate. - *

If this value is {@code true} for exactly one bean among multiple - * matching candidates, it will serve as a tie-breaker. + * Set whether this bean is a candidate for getting autowired into some other + * bean based on the plain type, without any further indications such as a + * qualifier match. + *

The default is {@code true}, allowing injection by type at any injection point. + * Switch this to {@code false} in order to restrict injection by default, + * effectively enforcing an additional indication such as a qualifier match. + * @since 6.2 + */ + public void setDefaultCandidate(boolean defaultCandidate) { + this.defaultCandidate = defaultCandidate; + } + + /** + * Return whether this bean is a candidate for getting autowired into some other + * bean based on the plain type, without any further indications such as a + * qualifier match? + *

The default is {@code true}. + * @since 6.2 + */ + public boolean isDefaultCandidate() { + return this.defaultCandidate; + } + + /** + * {@inheritDoc} + *

The default is {@code false}. */ @Override public void setPrimary(boolean primary) { @@ -705,13 +762,32 @@ public void setPrimary(boolean primary) { } /** - * Return whether this bean is a primary autowire candidate. + * {@inheritDoc} + *

The default is {@code false}. */ @Override public boolean isPrimary() { return this.primary; } + /** + * {@inheritDoc} + *

The default is {@code false}. + */ + @Override + public void setFallback(boolean fallback) { + this.fallback = fallback; + } + + /** + * {@inheritDoc} + *

The default is {@code false}. + */ + @Override + public boolean isFallback() { + return this.fallback; + } + /** * Register a qualifier to be used for autowire candidate resolution, * keyed by the qualifier's type name. @@ -731,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); } @@ -771,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; } @@ -816,9 +890,8 @@ public boolean isLenientConstructorResolution() { } /** - * Specify the factory bean to use, if any. - * This the name of the bean to call the specified factory method on. - * @see #setFactoryMethodName + * {@inheritDoc} + * @see #setBeanClass */ @Override public void setFactoryBeanName(@Nullable String factoryBeanName) { @@ -826,21 +899,19 @@ public void setFactoryBeanName(@Nullable String factoryBeanName) { } /** - * Return the factory bean name, if any. + * {@inheritDoc} + * @see #getBeanClass() */ @Override - @Nullable - public String getFactoryBeanName() { + public @Nullable String getFactoryBeanName() { return this.factoryBeanName; } /** - * Specify a factory method, if any. This method will be invoked with - * constructor arguments, or with no arguments if none are specified. - * The method will be invoked on the specified factory bean, if any, - * or otherwise as a static method on the local bean class. - * @see #setFactoryBeanName - * @see #setBeanClassName + * {@inheritDoc} + * @see RootBeanDefinition#setUniqueFactoryMethodName + * @see RootBeanDefinition#setNonUniqueFactoryMethodName + * @see RootBeanDefinition#setResolvedFactoryMethod */ @Override public void setFactoryMethodName(@Nullable String factoryMethodName) { @@ -848,11 +919,11 @@ public void setFactoryMethodName(@Nullable String factoryMethodName) { } /** - * Return a factory method, if any. + * {@inheritDoc} + * @see RootBeanDefinition#getResolvedFactoryMethod() */ @Override - @Nullable - public String getFactoryMethodName() { + public @Nullable String getFactoryMethodName() { return this.factoryMethodName; } @@ -864,7 +935,8 @@ public void setConstructorArgumentValues(ConstructorArgumentValues constructorAr } /** - * Return constructor argument values for this bean (never {@code null}). + * {@inheritDoc} + * @see #setConstructorArgumentValues */ @Override public ConstructorArgumentValues getConstructorArgumentValues() { @@ -877,7 +949,8 @@ public ConstructorArgumentValues getConstructorArgumentValues() { } /** - * Return if there are constructor argument values defined for this bean. + * {@inheritDoc} + * @see #setConstructorArgumentValues */ @Override public boolean hasConstructorArgumentValues() { @@ -892,7 +965,8 @@ public void setPropertyValues(MutablePropertyValues propertyValues) { } /** - * Return property values for this bean (never {@code null}). + * {@inheritDoc} + * @see #setPropertyValues */ @Override public MutablePropertyValues getPropertyValues() { @@ -905,8 +979,8 @@ public MutablePropertyValues getPropertyValues() { } /** - * Return if there are property values defined for this bean. - * @since 5.0.2 + * {@inheritDoc} + * @see #setPropertyValues */ @Override public boolean hasPropertyValues() { @@ -943,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; } @@ -951,13 +1025,12 @@ 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; } /** - * Set the name of the initializer method. + * {@inheritDoc} *

The default is {@code null} in which case there is no initializer method. * @see #setInitMethodNames */ @@ -967,11 +1040,11 @@ public void setInitMethodName(@Nullable String initMethodName) { } /** - * Return the name of the initializer method (the first one in case of multiple methods). + * {@inheritDoc} + *

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); } @@ -979,7 +1052,7 @@ public String getInitMethodName() { * Specify whether the configured initializer method is the default. *

The default value is {@code true} for a locally specified init method * but switched to {@code false} for a shared setting in a defaults section - * (e.g. {@code bean init-method} versus {@code beans default-init-method} + * (for example, {@code bean init-method} versus {@code beans default-init-method} * level in XML) which might not apply to all contained bean definitions. * @see #setInitMethodName * @see #applyDefaults @@ -1002,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; } @@ -1010,13 +1083,12 @@ 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; } /** - * Set the name of the destroy method. + * {@inheritDoc} *

The default is {@code null} in which case there is no destroy method. * @see #setDestroyMethodNames */ @@ -1026,11 +1098,11 @@ public void setDestroyMethodName(@Nullable String destroyMethodName) { } /** - * Return the name of the destroy method (the first one in case of multiple methods). + * {@inheritDoc} + *

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); } @@ -1038,7 +1110,7 @@ public String getDestroyMethodName() { * Specify whether the configured destroy method is the default. *

The default value is {@code true} for a locally specified destroy method * but switched to {@code false} for a shared setting in a defaults section - * (e.g. {@code bean destroy-method} versus {@code beans default-destroy-method} + * (for example, {@code bean destroy-method} versus {@code beans default-destroy-method} * level in XML) which might not apply to all contained bean definitions. * @see #setDestroyMethodName * @see #applyDefaults @@ -1073,7 +1145,8 @@ public boolean isSynthetic() { } /** - * Set the role hint for this {@code BeanDefinition}. + * {@inheritDoc} + *

The default is {@link #ROLE_APPLICATION}. */ @Override public void setRole(int role) { @@ -1081,7 +1154,8 @@ public void setRole(int role) { } /** - * Return the role hint for this {@code BeanDefinition}. + * {@inheritDoc} + *

The default is {@link #ROLE_APPLICATION}. */ @Override public int getRole() { @@ -1089,7 +1163,8 @@ public int getRole() { } /** - * Set a human-readable description of this bean definition. + * {@inheritDoc} + *

The default is no description. */ @Override public void setDescription(@Nullable String description) { @@ -1097,11 +1172,11 @@ public void setDescription(@Nullable String description) { } /** - * Return a human-readable description of this bean definition. + * {@inheritDoc} + *

The default is no description. */ @Override - @Nullable - public String getDescription() { + public @Nullable String getDescription() { return this.description; } @@ -1116,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; } @@ -1130,31 +1204,27 @@ public void setResourceDescription(@Nullable String resourceDescription) { } /** - * Return a description of the resource that this bean definition - * came from (for the purpose of showing context in case of errors). + * {@inheritDoc} + * @see #setResourceDescription */ @Override - @Nullable - public String getResourceDescription() { + public @Nullable String getResourceDescription() { return (this.resource != null ? this.resource.getDescription() : null); } /** - * Set the originating (e.g. decorated) BeanDefinition, if any. + * Set the originating (for example, decorated) BeanDefinition, if any. */ public void setOriginatingBeanDefinition(BeanDefinition originatingBd) { this.resource = new BeanDefinitionResource(originatingBd); } /** - * Return the originating BeanDefinition, or {@code null} if none. - * Allows for retrieving the decorated bean definition, if any. - *

Note that this method returns the immediate originator. Iterate through the - * originator chain to find the original BeanDefinition as defined by the user. + * {@inheritDoc} + * @see #setOriginatingBeanDefinition */ @Override - @Nullable - public BeanDefinition getOriginatingBeanDefinition() { + public @Nullable BeanDefinition getOriginatingBeanDefinition() { return (this.resource instanceof BeanDefinitionResource bdr ? bdr.getBeanDefinition() : null); } @@ -1284,8 +1354,7 @@ public int hashCode() { @Override public String toString() { - StringBuilder sb = new StringBuilder("class ["); - sb.append(getBeanClassName()).append(']'); + StringBuilder sb = new StringBuilder("class=").append(getBeanClassName()); sb.append("; scope=").append(this.scope); sb.append("; abstract=").append(this.abstractFlag); sb.append("; lazyInit=").append(this.lazyInit); @@ -1293,6 +1362,7 @@ public String toString() { sb.append("; dependencyCheck=").append(this.dependencyCheck); sb.append("; autowireCandidate=").append(this.autowireCandidate); sb.append("; primary=").append(this.primary); + sb.append("; fallback=").append(this.fallback); sb.append("; factoryBeanName=").append(this.factoryBeanName); sb.append("; factoryMethodName=").append(this.factoryMethodName); sb.append("; initMethodNames=").append(Arrays.toString(this.initMethodNames)); 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 1e283092c502..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 @@ -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,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -33,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; @@ -71,9 +71,9 @@ 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; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -116,27 +116,25 @@ 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); /** Custom PropertyEditorRegistrars to apply to the beans of this factory. */ private final Set propertyEditorRegistrars = new LinkedHashSet<>(4); @@ -145,34 +143,33 @@ 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 e.g. to annotation attribute values. */ + /** String resolvers to apply, for example, to annotation attribute values. */ private final List embeddedValueResolvers = new CopyOnWriteArrayList<>(); /** BeanPostProcessors to apply. */ 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); + /** Application startup metrics. */ + private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + /** Map from bean name to merged RootBeanDefinition. */ private final Map mergedBeanDefinitions = new ConcurrentHashMap<>(256); /** Names of beans that have already been created at least once. */ - private final Set alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256)); + private final Set alreadyCreated = ConcurrentHashMap.newKeySet(256); /** Names of beans that are currently in creation. */ private final ThreadLocal prototypesCurrentlyInCreation = new NamedThreadLocal<>("Prototype beans currently in creation"); - /** Application startup metrics. **/ - private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; /** * Create a new AbstractBeanFactory. @@ -205,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); } @@ -218,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); @@ -237,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); @@ -315,6 +312,17 @@ else if (requiredType != null) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", ex); } + catch (BeanCreationException ex) { + if (requiredType != null) { + // Wrap exception with current bean metadata but only if specifically + // requested (indicated by required type), not for depends-on cascades. + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Failed to initialize dependency '" + ex.getBeanName() + "' of " + + requiredType.getSimpleName() + " bean '" + beanName + "': " + + ex.getMessage(), ex); + } + throw ex; + } } } @@ -524,42 +532,73 @@ protected boolean isTypeMatch(String name, ResolvableType typeToMatch, boolean a // Check manually registered singletons. Object beanInstance = getSingleton(beanName, false); if (beanInstance != null && beanInstance.getClass() != NullBean.class) { + + // Determine target for FactoryBean match if necessary. if (beanInstance instanceof FactoryBean factoryBean) { if (!isFactoryDereference) { Class type = getTypeForFactoryBean(factoryBean); - return (type != null && typeToMatch.isAssignableFrom(type)); - } - else { - return typeToMatch.isInstance(beanInstance); - } - } - else if (!isFactoryDereference) { - if (typeToMatch.isInstance(beanInstance)) { - // Direct match for exposed instance? - return true; - } - else if (typeToMatch.hasGenerics() && containsBeanDefinition(beanName)) { - // Generics potentially only match on the target class, not on the proxy... - RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); - Class targetType = mbd.getTargetType(); - if (targetType != null && targetType != ClassUtils.getUserClass(beanInstance)) { - // Check raw class match as well, making sure it's exposed on the proxy. - Class classToMatch = typeToMatch.resolve(); - if (classToMatch != null && !classToMatch.isInstance(beanInstance)) { + if (type == null) { + return false; + } + if (typeToMatch.isAssignableFrom(type)) { + return true; + } + else if (typeToMatch.hasGenerics() && containsBeanDefinition(beanName)) { + RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + ResolvableType targetType = mbd.targetType; + if (targetType == null) { + targetType = mbd.factoryMethodReturnType; + } + if (targetType == null) { return false; } - if (typeToMatch.isAssignableFrom(targetType)) { - return true; + Class targetClass = targetType.resolve(); + if (targetClass != null && FactoryBean.class.isAssignableFrom(targetClass)) { + Class classToMatch = typeToMatch.resolve(); + if (classToMatch != null && !FactoryBean.class.isAssignableFrom(classToMatch) && + !classToMatch.isAssignableFrom(targetType.toClass())) { + return typeToMatch.isAssignableFrom(targetType.getGeneric()); + } + } + else { + return typeToMatch.isAssignableFrom(targetType); } } - ResolvableType resolvableType = mbd.targetType; - if (resolvableType == null) { - resolvableType = mbd.factoryMethodReturnType; + return false; + } + } + else if (isFactoryDereference) { + return false; + } + + // Actual matching against bean instance... + if (typeToMatch.isInstance(beanInstance)) { + // Direct match for exposed instance? + return true; + } + else if (typeToMatch.hasGenerics() && containsBeanDefinition(beanName)) { + // Generics potentially only match on the target class, not on the proxy... + RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + Class targetType = mbd.getTargetType(); + if (targetType != null && targetType != ClassUtils.getUserClass(beanInstance)) { + // Check raw class match as well, making sure it's exposed on the proxy. + Class classToMatch = typeToMatch.resolve(); + if (classToMatch != null && !classToMatch.isInstance(beanInstance)) { + return false; + } + if (typeToMatch.isAssignableFrom(targetType)) { + return true; } - return (resolvableType != null && typeToMatch.isAssignableFrom(resolvableType)); } + ResolvableType resolvableType = mbd.targetType; + if (resolvableType == null) { + resolvableType = mbd.factoryMethodReturnType; + } + return (resolvableType != null && typeToMatch.isAssignableFrom(resolvableType)); + } + else { + return false; } - return false; } else if (containsSingleton(beanName) && !containsBeanDefinition(beanName)) { // null instance registered @@ -661,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. @@ -725,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)) { @@ -756,8 +793,7 @@ public String[] getAliases(String name) { //--------------------------------------------------------------------- @Override - @Nullable - public BeanFactory getParentBeanFactory() { + public @Nullable BeanFactory getParentBeanFactory() { return this.parentBeanFactory; } @@ -790,8 +826,7 @@ public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { } @Override - @Nullable - public ClassLoader getBeanClassLoader() { + public @Nullable ClassLoader getBeanClassLoader() { return this.beanClassLoader; } @@ -801,8 +836,7 @@ public void setTempClassLoader(@Nullable ClassLoader tempClassLoader) { } @Override - @Nullable - public ClassLoader getTempClassLoader() { + public @Nullable ClassLoader getTempClassLoader() { return this.tempClassLoader; } @@ -822,8 +856,7 @@ public void setBeanExpressionResolver(@Nullable BeanExpressionResolver resolver) } @Override - @Nullable - public BeanExpressionResolver getBeanExpressionResolver() { + public @Nullable BeanExpressionResolver getBeanExpressionResolver() { return this.beanExpressionResolver; } @@ -833,15 +866,19 @@ public void setConversionService(@Nullable ConversionService conversionService) } @Override - @Nullable - public ConversionService getConversionService() { + public @Nullable ConversionService getConversionService() { return this.conversionService; } @Override public void addPropertyEditorRegistrar(PropertyEditorRegistrar registrar) { Assert.notNull(registrar, "PropertyEditorRegistrar must not be null"); - this.propertyEditorRegistrars.add(registrar); + if (registrar.overridesDefaultEditors()) { + this.defaultEditorRegistrars.add(registrar); + } + else { + this.propertyEditorRegistrars.add(registrar); + } } /** @@ -879,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; } @@ -911,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; } @@ -1047,15 +1082,14 @@ 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); } @Override public void setApplicationStartup(ApplicationStartup applicationStartup) { - Assert.notNull(applicationStartup, "applicationStartup must not be null"); + Assert.notNull(applicationStartup, "ApplicationStartup must not be null"); this.applicationStartup = applicationStartup; } @@ -1072,6 +1106,7 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { setBeanExpressionResolver(otherFactory.getBeanExpressionResolver()); setConversionService(otherFactory.getConversionService()); if (otherFactory instanceof AbstractBeanFactory otherAbstractFactory) { + this.defaultEditorRegistrars.addAll(otherAbstractFactory.defaultEditorRegistrars); this.propertyEditorRegistrars.addAll(otherAbstractFactory.propertyEditorRegistrars); this.customEditors.putAll(otherAbstractFactory.customEditors); this.typeConverter = otherAbstractFactory.typeConverter; @@ -1102,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. @@ -1153,7 +1188,7 @@ protected void beforePrototypeCreation(String beanName) { this.prototypesCurrentlyInCreation.set(beanName); } else if (curVal instanceof String strValue) { - Set beanNameSet = new HashSet<>(2); + Set beanNameSet = CollectionUtils.newHashSet(2); beanNameSet.add(strValue); beanNameSet.add(beanName); this.prototypesCurrentlyInCreation.set(beanNameSet); @@ -1241,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; @@ -1271,29 +1306,18 @@ protected void initBeanWrapper(BeanWrapper bw) { protected void registerCustomEditors(PropertyEditorRegistry registry) { if (registry instanceof PropertyEditorRegistrySupport registrySupport) { registrySupport.useConfigValueEditors(); + if (!this.defaultEditorRegistrars.isEmpty()) { + // Optimization: lazy overriding of default editors only when needed + registrySupport.setDefaultEditorRegistrar(new BeanFactoryDefaultEditorRegistrar()); + } + } + else if (!this.defaultEditorRegistrars.isEmpty()) { + // Fallback: proactive overriding of default editors + applyEditorRegistrars(registry, this.defaultEditorRegistrars); } + if (!this.propertyEditorRegistrars.isEmpty()) { - for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) { - try { - registrar.registerCustomEditors(registry); - } - catch (BeanCreationException ex) { - Throwable rootCause = ex.getMostSpecificCause(); - if (rootCause instanceof BeanCurrentlyInCreationException bce) { - String bceBeanName = bce.getBeanName(); - if (bceBeanName != null && isCurrentlyInCreation(bceBeanName)) { - if (logger.isDebugEnabled()) { - logger.debug("PropertyEditorRegistrar [" + registrar.getClass().getName() + - "] failed because it tried to obtain currently created bean '" + - ex.getBeanName() + "': " + ex.getMessage()); - } - onSuppressedException(ex); - continue; - } - } - throw ex; - } - } + applyEditorRegistrars(registry, this.propertyEditorRegistrars); } if (!this.customEditors.isEmpty()) { this.customEditors.forEach((requiredType, editorClass) -> @@ -1301,6 +1325,29 @@ protected void registerCustomEditors(PropertyEditorRegistry registry) { } } + private void applyEditorRegistrars(PropertyEditorRegistry registry, Set registrars) { + for (PropertyEditorRegistrar registrar : registrars) { + try { + registrar.registerCustomEditors(registry); + } + catch (BeanCreationException ex) { + Throwable rootCause = ex.getMostSpecificCause(); + if (rootCause instanceof BeanCurrentlyInCreationException bce) { + String bceBeanName = bce.getBeanName(); + if (bceBeanName != null && isCurrentlyInCreation(bceBeanName)) { + if (logger.isDebugEnabled()) { + logger.debug("PropertyEditorRegistrar [" + registrar.getClass().getName() + + "] failed because it tried to obtain currently created bean '" + + ex.getBeanName() + "': " + ex.getMessage()); + } + onSuppressedException(ex); + return; + } + } + throw ex; + } + } + } /** * Return a merged RootBeanDefinition, traversing the parent bean definition @@ -1411,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) { @@ -1440,17 +1487,26 @@ 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. * @param mbd the merged bean definition to check * @param beanName the name of the bean * @param args the arguments for bean creation, if any - * @throws BeanDefinitionStoreException in case of validation failure */ - protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) - throws BeanDefinitionStoreException { - + protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object @Nullable [] args) { if (mbd.isAbstract()) { throw new BeanIsAbstractException(beanName); } @@ -1472,7 +1528,7 @@ protected void clearMergedBeanDefinition(String beanName) { * Clear the merged bean definition cache, removing entries for beans * which are not considered eligible for full metadata caching yet. *

Typically triggered after changes to the original bean definitions, - * e.g. after applying a {@code BeanFactoryPostProcessor}. Note that metadata + * for example, after applying a {@code BeanFactoryPostProcessor}. Note that metadata * for beans which have already been created at this point will be kept around. * @since 4.2 */ @@ -1495,15 +1551,18 @@ 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 { if (mbd.hasBeanClass()) { return mbd.getBeanClass(); } - return doResolveBeanClass(mbd, typesToMatch); + Class beanClass = doResolveBeanClass(mbd, typesToMatch); + if (mbd.hasBeanClass()) { + mbd.prepareMethodOverrides(); + } + return beanClass; } catch (ClassNotFoundException ex) { throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), ex); @@ -1511,10 +1570,13 @@ protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Cla catch (LinkageError err) { throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), err); } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbd.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } } - @Nullable - private Class doResolveBeanClass(RootBeanDefinition mbd, Class... typesToMatch) + private @Nullable Class doResolveBeanClass(RootBeanDefinition mbd, Class... typesToMatch) throws ClassNotFoundException { ClassLoader beanClassLoader = getBeanClassLoader(); @@ -1523,7 +1585,7 @@ private Class doResolveBeanClass(RootBeanDefinition mbd, Class... typesToM if (!ObjectUtils.isEmpty(typesToMatch)) { // When just doing type checks (i.e. not creating an actual instance yet), - // use the specified temporary class loader (e.g. in a weaving scenario). + // use the specified temporary class loader (for example, in a weaving scenario). ClassLoader tempClassLoader = getTempClassLoader(); if (tempClassLoader != null) { dynamicLoader = tempClassLoader; @@ -1581,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; } @@ -1613,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; @@ -1646,7 +1706,7 @@ protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) { * already. The implementation is allowed to instantiate the target factory bean if * {@code allowInit} is {@code true} and the type cannot be determined another way; * otherwise it is restricted to introspecting signatures and related metadata. - *

If no {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE} if set on the bean definition + *

If no {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE} is set on the bean definition * and {@code allowInit} is {@code true}, the default implementation will create * the FactoryBean via {@code getBean} to call its {@code getObjectType} method. * Subclasses are encouraged to optimize this, typically by inspecting the generic @@ -1665,9 +1725,15 @@ protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) { * @see #getBean(String) */ protected ResolvableType getTypeForFactoryBean(String beanName, RootBeanDefinition mbd, boolean allowInit) { - ResolvableType result = getTypeForFactoryBeanFromAttributes(mbd); - if (result != ResolvableType.NONE) { - return result; + try { + ResolvableType result = getTypeForFactoryBeanFromAttributes(mbd); + if (result != ResolvableType.NONE) { + return result; + } + } + catch (IllegalArgumentException ex) { + throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName, + String.valueOf(ex.getMessage())); } if (allowInit && mbd.isSingleton()) { @@ -1925,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; @@ -2042,4 +2108,20 @@ static class BeanPostProcessorCache { final List mergedDefinition = new ArrayList<>(); } + + /** + * {@link PropertyEditorRegistrar} that delegates to the bean factory's + * default registrars, adding exception handling for circular reference + * scenarios where an editor tries to refer back to the currently created bean. + * + * @since 6.2.3 + */ + class BeanFactoryDefaultEditorRegistrar implements PropertyEditorRegistrar { + + @Override + public void registerCustomEditors(PropertyEditorRegistry registry) { + applyEditorRegistrars(registry, defaultEditorRegistrars); + } + } + } 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 e7eafe4158c8..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 @@ -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,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 @@ -50,7 +51,7 @@ default boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDes *

The default implementation checks {@link DependencyDescriptor#isRequired()}. * @param descriptor the descriptor for the target method parameter or field * @return whether the descriptor is marked as required or possibly indicating - * non-required status some other way (e.g. through a parameter annotation) + * non-required status some other way (for example, through a parameter annotation) * @since 5.0 * @see DependencyDescriptor#isRequired() */ @@ -72,6 +73,17 @@ default boolean hasQualifier(DependencyDescriptor descriptor) { return false; } + /** + * Determine whether a target bean name is suggested for the given dependency + * (typically - but not necessarily - declared with a single-value qualifier). + * @param descriptor the descriptor for the target method parameter or field + * @return the qualifier value, if any + * @since 6.2 + */ + default @Nullable String getSuggestedName(DependencyDescriptor descriptor) { + return null; + } + /** * Determine whether a default value is suggested for the given dependency. *

The default implementation simply returns {@code null}. @@ -80,8 +92,7 @@ default boolean hasQualifier(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; } @@ -95,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; } @@ -109,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 6baa1fd13880..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 @@ -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. @@ -32,10 +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; @@ -122,7 +126,7 @@ public static boolean isSetterDefinedInInterface(PropertyDescriptor pd, Set req * the given {@code method} does not declare any {@linkplain * Method#getTypeParameters() formal type variables} *

  • the {@linkplain Method#getReturnType() standard return type}, if the - * target return type cannot be inferred (e.g., due to type erasure)
  • + * target return type cannot be inferred (for example, due to type erasure) *
  • {@code null}, if the length of the given arguments array is shorter * than the length of the {@linkplain * Method#getGenericParameterTypes() formal argument list} for the given @@ -172,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"); @@ -182,8 +186,8 @@ public static Class resolveReturnTypeForFactoryMethod( Type[] methodParameterTypes = method.getGenericParameterTypes(); Assert.isTrue(args.length == methodParameterTypes.length, "Argument array does not match parameter count"); - // Ensure that the type variable (e.g., T) is declared directly on the method - // itself (e.g., via ), not on the enclosing class or interface. + // Ensure that the type variable (for example, T) is declared directly on the method + // itself (for example, via ), not on the enclosing class or interface. boolean locallyDeclaredTypeVariableMatchesReturnType = false; for (TypeVariable currentTypeVariable : declaredTypeVariables) { if (currentTypeVariable.equals(genericReturnType)) { @@ -259,6 +263,43 @@ else if (arg instanceof TypedStringValue typedValue) { return method.getReturnType(); } + /** + * Check the autowire-candidate status for the specified bean. + * @param beanFactory the bean factory + * @param beanName the name of the bean to check + * @return whether the specified bean qualifies as an autowire candidate + * @since 6.2.3 + * @see org.springframework.beans.factory.config.BeanDefinition#isAutowireCandidate() + */ + public static boolean isAutowireCandidate(ConfigurableBeanFactory beanFactory, String beanName) { + try { + return beanFactory.getMergedBeanDefinition(beanName).isAutowireCandidate(); + } + catch (NoSuchBeanDefinitionException ex) { + // A manually registered singleton instance not backed by a BeanDefinition. + return true; + } + } + + /** + * Check the default-candidate status for the specified bean. + * @param beanFactory the bean factory + * @param beanName the name of the bean to check + * @return whether the specified bean qualifies as a default candidate + * @since 6.2.4 + * @see AbstractBeanDefinition#isDefaultCandidate() + */ + public static boolean isDefaultCandidate(ConfigurableBeanFactory beanFactory, String beanName) { + try { + BeanDefinition mbd = beanFactory.getMergedBeanDefinition(beanName); + return (!(mbd instanceof AbstractBeanDefinition abd) || abd.isDefaultCandidate()); + } + catch (NoSuchBeanDefinitionException ex) { + // A manually registered singleton instance not backed by a BeanDefinition. + return true; + } + } + /** * Reflective {@link InvocationHandler} for lazy access to the current target object. 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 362737c9ee3a..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 @@ -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,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; /** @@ -34,6 +35,7 @@ * @author Rod Johnson * @author Rob Harrop * @author Juergen Hoeller + * @author Yanming Zhou * @since 2.0 */ public final class BeanDefinitionBuilder { @@ -348,6 +350,15 @@ public BeanDefinitionBuilder setPrimary(boolean primary) { return this; } + /** + * Set whether this bean is a fallback autowire candidate. + * @since 6.2 + */ + public BeanDefinitionBuilder setFallback(boolean fallback) { + this.beanDefinition.setFallback(fallback); + return this; + } + /** * Set the role of this definition. */ 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 5da53bfe3b67..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 @@ -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,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; /** @@ -59,7 +57,7 @@ public void setLazyInit(boolean lazyInit) { * @return whether to apply lazy-init semantics ({@code false} by default) */ public boolean isLazyInit() { - return (this.lazyInit != null && this.lazyInit.booleanValue()); + return (this.lazyInit != null && this.lazyInit); } /** @@ -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 f894298b151a..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 @@ -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,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 @@ -54,12 +53,27 @@ public BeanDefinitionOverrideException( this.existingDefinition = existingDefinition; } + /** + * Create a new BeanDefinitionOverrideException for the given new and existing definition. + * @param beanName the name of the bean + * @param beanDefinition the newly registered bean definition + * @param existingDefinition the existing bean definition for the same name + * @param msg the detail message to include + * @since 6.2.1 + */ + public BeanDefinitionOverrideException( + String beanName, BeanDefinition beanDefinition, BeanDefinition existingDefinition, String msg) { + + super(beanDefinition.getResourceDescription(), beanName, msg); + this.beanDefinition = beanDefinition; + this.existingDefinition = existingDefinition; + } + /** * Return the description of the resource that the bean definition came from. */ @Override - @NonNull public String getResourceDescription() { return String.valueOf(super.getResourceDescription()); } @@ -68,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..b6eeb3ae2323 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 @@ -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 @@ -63,8 +64,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 +72,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 d2d0d947d34c..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-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.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. @@ -466,7 +460,7 @@ private List resolveManagedList(Object argName, List ml) { * For each element in the managed set, resolve reference if necessary. */ private Set resolveManagedSet(Object argName, Set ms) { - Set resolved = new LinkedHashSet<>(ms.size()); + Set resolved = CollectionUtils.newLinkedHashSet(ms.size()); int i = 0; for (Object m : ms) { resolved.add(resolveValueIfNecessary(new KeyedArgName(argName, i), m)); 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 b3da7fb62b44..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-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 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,7 +242,7 @@ public LookupOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanFa } @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 { // Cast is safe, as CallbackFilter filters are used selectively. LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method); Assert.state(lo != null, "LookupOverride not found"); @@ -264,7 +265,7 @@ public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp /** * CGLIB MethodInterceptor to override methods, replacing them with a call - * to a generic MethodReplacer. + * to a generic {@link MethodReplacer}. */ private static class ReplaceOverrideMethodInterceptor extends CglibIdentitySupport implements MethodInterceptor { @@ -276,12 +277,20 @@ public ReplaceOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanF } @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 mr.reimplement(obj, method, args); + return processReturnType(method, mr.reimplement(obj, method, args)); + } + + private @Nullable T processReturnType(Method method, @Nullable T returnValue) { + Class returnType = method.getReturnType(); + if (returnValue == null && returnType != void.class && returnType.isPrimitive()) { + throw new IllegalStateException( + "Null return value from MethodReplacer does not match primitive return type for: " + method); + } + return returnValue; } } 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 26bc22becb59..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-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 @@ 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; @@ -63,13 +64,14 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.core.CollectionFactory; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; 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; import org.springframework.util.MethodInvoker; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -121,11 +123,7 @@ public ConstructorResolver(AbstractAutowireCapableBeanFactory beanFactory) { /** * "autowire constructor" (with constructor arguments by type) behavior. - * Also applied if explicit constructor argument values are specified, - * matching all remaining arguments with beans from the bean factory. - *

    This corresponds to constructor injection: In this mode, a Spring - * bean factory is able to host components that expect constructor-based - * dependency resolution. + * Also applied if explicit constructor argument values are specified. * @param beanName the name of the bean * @param mbd the merged bean definition for the bean * @param chosenCtors chosen candidate constructors (or {@code null} if none) @@ -133,15 +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") // 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; @@ -228,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) { @@ -394,8 +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") // 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) { @@ -602,7 +602,7 @@ else if (factoryMethodToUse != null && typeDiffWeight == minTypeDiffWeight && } } else if (resolvedValues != null) { - Set valueHolders = new LinkedHashSet<>(resolvedValues.getArgumentCount()); + Set valueHolders = CollectionUtils.newLinkedHashSet(resolvedValues.getArgumentCount()); valueHolders.addAll(resolvedValues.getIndexedArgumentValues().values()); valueHolders.addAll(resolvedValues.getGenericArgumentValues()); for (ValueHolder value : valueHolders) { @@ -614,19 +614,21 @@ else if (resolvedValues != null) { String argDesc = StringUtils.collectionToCommaDelimitedString(argTypes); throw new BeanCreationException(mbd.getResourceDescription(), beanName, "No matching factory method found on class [" + factoryClass.getName() + "]: " + - (mbd.getFactoryBeanName() != null ? - "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + + (mbd.getFactoryBeanName() != null ? "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + "factory method '" + mbd.getFactoryMethodName() + "(" + argDesc + ")'. " + - "Check that a method with the specified name " + - (minNrOfArgs > 0 ? "and arguments " : "") + - "exists and that it is " + - (isStatic ? "static" : "non-static") + "."); + "Check that a method with the specified name " + (minNrOfArgs > 0 ? "and arguments " : "") + + "exists and that it is " + (isStatic ? "static" : "non-static") + "."); } else if (void.class == factoryMethodToUse.getReturnType()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid factory method '" + mbd.getFactoryMethodName() + "' on class [" + factoryClass.getName() + "]: needs to have a non-void return type!"); } + 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!"); + } else if (ambiguousFactoryMethods != null) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Ambiguous factory method matches found on class [" + factoryClass.getName() + "] " + @@ -645,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( @@ -717,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(); @@ -812,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(); @@ -821,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]; @@ -895,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)) { @@ -916,7 +917,7 @@ Object resolveAutowiredArgument(DependencyDescriptor descriptor, Class paramT catch (NoSuchBeanDefinitionException ex) { if (fallback) { // Single constructor or factory method -> let's return an empty array/collection - // for e.g. a vararg or a non-null List/Set/Map parameter. + // for example, a vararg or a non-null List/Set/Map parameter. if (paramType.isArray()) { return Array.newInstance(paramType.componentType(), 0); } @@ -1039,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()); @@ -1087,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) { @@ -1147,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) { @@ -1200,8 +1198,7 @@ private boolean isMatch(ResolvableType parameterType, ResolvableType valueType, } private Predicate isAssignable(ResolvableType valueType) { - return parameterType -> (valueType == ResolvableType.NONE - || parameterType.isAssignableFrom(valueType)); + return parameterType -> (valueType == ResolvableType.NONE || parameterType.isAssignableFrom(valueType)); } private ResolvableType extractElementType(ResolvableType parameterType) { @@ -1240,8 +1237,8 @@ private Predicate valueOrCollection(ResolvableType valueType, /** * Return a {@link Predicate} for a parameter type that checks if its target * value is a {@link Class} and the value type is a {@link String}. This is - * a regular use cases where a {@link Class} is defined in the bean - * definition as an FQN. + * a regular use case where a {@link Class} is defined in the bean definition + * as a fully-qualified class name. * @param valueType the type of the value * @return a predicate to indicate a fallback match for a String to Class * parameter @@ -1256,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); } @@ -1287,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; @@ -1322,7 +1317,7 @@ else if (ctors.length == 0) { // No public constructors -> check non-public ctors = clazz.getDeclaredConstructors(); if (ctors.length == 1) { - // A single non-public constructor, e.g. from a non-public record type + // A single non-public constructor, for example, from a non-public record type return ctors; } } @@ -1336,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; @@ -1350,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; @@ -1400,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(); @@ -1426,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); @@ -1442,10 +1435,15 @@ public boolean hasShortcut() { } @Override - public Object resolveShortcut(BeanFactory beanFactory) { + public @Nullable Object resolveShortcut(BeanFactory beanFactory) { String shortcut = this.shortcut; return (shortcut != null ? beanFactory.getBean(shortcut, getDependencyType()) : null); } + + @Override + public boolean usesStandardBeanLookup() { + return true; + } } 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 20d406a54727..2d3ef536e81a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,13 +37,17 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; import jakarta.inject.Provider; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; @@ -69,19 +73,23 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.NamedBeanHolder; +import org.springframework.core.NamedThreadLocal; 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.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.CompositeIterator; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -110,6 +118,7 @@ * @author Chris Beams * @author Phillip Webb * @author Stephane Nicoll + * @author Sebastien Deleuze * @since 16 April 2001 * @see #registerBeanDefinition * @see #addBeanPostProcessor @@ -120,17 +129,30 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { - @Nullable - private static Class javaxInjectProviderClass; + /** + * 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 { - javaxInjectProviderClass = + jakartaInjectProviderClass = ClassUtils.forName("jakarta.inject.Provider", DefaultListableBeanFactory.class.getClassLoader()); } catch (ClassNotFoundException ex) { // JSR-330 API not available - Provider interface simply not supported then. - javaxInjectProviderClass = null; + jakartaInjectProviderClass = null; } } @@ -139,19 +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 @Nullable final 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. */ - private boolean allowBeanDefinitionOverriding = true; + private @Nullable Boolean allowBeanDefinitionOverriding; /** Whether to allow eager class loading even for lazy-init beans. */ private boolean allowEagerClassLoading = true; + 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; @@ -165,6 +190,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto /** Map from bean name to merged BeanDefinitionHolder. */ private final Map mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256); + /** Set of bean definition names with a primary marker. */ + private final Set primaryBeanNames = ConcurrentHashMap.newKeySet(16); + /** Map of singleton and non-singleton bean names, keyed by dependency type. */ private final Map, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64); @@ -178,12 +206,17 @@ 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; + /** 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"); + /** * Create a new DefaultListableBeanFactory. @@ -220,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; } @@ -242,7 +274,7 @@ public void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverridi * @since 4.1.2 */ public boolean isAllowBeanDefinitionOverriding() { - return this.allowBeanDefinitionOverriding; + return !Boolean.FALSE.equals(this.allowBeanDefinitionOverriding); } /** @@ -268,6 +300,16 @@ public boolean isAllowEagerClassLoading() { return this.allowEagerClassLoading; } + @Override + public void setBootstrapExecutor(@Nullable Executor bootstrapExecutor) { + this.bootstrapExecutor = bootstrapExecutor; + } + + @Override + public @Nullable Executor getBootstrapExecutor() { + return this.bootstrapExecutor; + } + /** * Set a {@link java.util.Comparator} for dependency Lists and arrays. * @since 4.0 @@ -282,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; } @@ -314,10 +355,11 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { if (otherFactory instanceof DefaultListableBeanFactory otherListableFactory) { this.allowBeanDefinitionOverriding = otherListableFactory.allowBeanDefinitionOverriding; this.allowEagerClassLoading = otherListableFactory.allowEagerClassLoading; + this.bootstrapExecutor = otherListableFactory.bootstrapExecutor; this.dependencyComparator = otherListableFactory.dependencyComparator; // A clone of the AutowireCandidateResolver since it is potentially BeanFactoryAware setAutowireCandidateResolver(otherListableFactory.getAutowireCandidateResolver().cloneIfNecessary()); - // Make resolvable dependencies (e.g. ResourceLoader) available here as well + // Make resolvable dependencies (for example, ResourceLoader) available here as well this.resolvableDependencies.putAll(otherListableFactory.resolvableDependencies); } } @@ -334,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) { @@ -399,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); @@ -407,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); } @@ -430,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); } @@ -455,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(); } @@ -476,18 +516,43 @@ public Stream orderedStream() { Stream stream = matchingBeans.values().stream(); return stream.sorted(adaptOrderComparator(matchingBeans)); } + @SuppressWarnings("unchecked") + @Override + 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, boolean includeNonSingletons) { + String[] beanNames = beanNamesForStream(requiredType, includeNonSingletons, allowEagerInit); + if (beanNames.length == 0) { + return Stream.empty(); + } + Map matchingBeans = CollectionUtils.newLinkedHashMap(beanNames.length); + for (String beanName : beanNames) { + if (customFilter.test(getType(beanName))) { + Object beanInstance = getBean(beanName); + if (!(beanInstance instanceof NullBean)) { + matchingBeans.put(beanName, (T) beanInstance); + } + } + } + return matchingBeans.values().stream().sorted(adaptOrderComparator(matchingBeans)); + } }; } - @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(); } BeanFactory parent = getParentBeanFactory(); - if (parent instanceof DefaultListableBeanFactory dlfb) { - return dlfb.resolveBean(requiredType, args, nonUniqueAsNull); + if (parent instanceof DefaultListableBeanFactory dlbf) { + return dlbf.resolveBean(requiredType, args, nonUniqueAsNull); } else if (parent != null) { ObjectProvider parentProvider = parent.getBeanProvider(requiredType); @@ -501,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 @@ -568,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; @@ -717,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 { @@ -740,7 +808,7 @@ public A findAnnotationOnBean( } if (containsBeanDefinition(beanName)) { RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); - // Check raw bean class, e.g. in case of a proxy. + // Check raw bean class, for example, in case of a proxy. if (bd.hasBeanClass() && bd.getFactoryMethodName() == null) { Class beanClass = bd.getBeanClass(); if (beanClass != beanType) { @@ -779,7 +847,7 @@ public Set findAllAnnotationsOnBean( } if (containsBeanDefinition(beanName)) { RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); - // Check raw bean class, e.g. in case of a proxy. + // Check raw bean class, for example, in case of a proxy. if (bd.hasBeanClass() && bd.getFactoryMethodName() == null) { Class beanClass = bd.getBeanClass(); if (beanClass != beanType) { @@ -837,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); } @@ -871,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); @@ -939,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) { @@ -949,6 +1016,72 @@ protected Object obtainInstanceFromSupplier(Supplier supplier, String beanNam return super.obtainInstanceFromSupplier(supplier, beanName, mbd); } + @Override + protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) { + super.cacheMergedBeanDefinition(mbd, beanName); + if (mbd.isPrimary()) { + this.primaryBeanNames.add(beanName); + } + } + + @Override + protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object @Nullable [] args) { + super.checkMergedBeanDefinition(mbd, beanName, args); + + if (mbd.isBackgroundInit()) { + if (this.preInstantiationThread.get() == PreInstantiation.MAIN && getBootstrapExecutor() != null) { + throw new BeanCurrentlyInCreationException(beanName, "Bean marked for background " + + "initialization but requested in mainline thread - declare ObjectProvider " + + "or lazy injection point in dependent mainline beans"); + } + } + else { + // 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 " + + "through depends-on '" + beanName + "' declaration for dependent background beans"); + } + } + } + + @Override + protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { + String mainThreadPrefix = this.mainThreadPrefix; + if (this.mainThreadPrefix != null) { + // We only differentiate in the preInstantiateSingletons 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 (mainThreadPrefix != null && !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 public void preInstantiateSingletons() throws BeansException { if (logger.isTraceEnabled()) { @@ -960,24 +1093,38 @@ public void preInstantiateSingletons() throws BeansException { List beanNames = new ArrayList<>(this.beanDefinitionNames); // Trigger initialization of all non-lazy singleton beans... - for (String beanName : beanNames) { - RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); - if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { - if (isFactoryBean(beanName)) { - Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); - if (bean instanceof SmartFactoryBean smartFactoryBean && smartFactoryBean.isEagerInit()) { - getBean(beanName); + List> futures = new ArrayList<>(); + + this.preInstantiationThread.set(PreInstantiation.MAIN); + this.mainThreadPrefix = getThreadNamePrefix(); + try { + for (String beanName : beanNames) { + RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + if (!mbd.isAbstract() && mbd.isSingleton()) { + CompletableFuture future = preInstantiateSingleton(beanName, mbd); + if (future != null) { + futures.add(future); } } - else { - getBean(beanName); - } + } + } + finally { + this.mainThreadPrefix = null; + this.preInstantiationThread.remove(); + } + + if (!futures.isEmpty()) { + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + } + catch (CompletionException ex) { + ReflectionUtils.rethrowRuntimeException(ex.getCause()); } } // Trigger post-initialization callback for all applicable beans... for (String beanName : beanNames) { - Object singletonInstance = getSingleton(beanName); + Object singletonInstance = getSingleton(beanName, false); if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) { StartupStep smartInitialize = getApplicationStartup().start("spring.beans.smart-initialize") .tag("beanName", beanName); @@ -987,6 +1134,81 @@ public void preInstantiateSingletons() throws BeansException { } } + private @Nullable CompletableFuture preInstantiateSingleton(String beanName, RootBeanDefinition mbd) { + if (mbd.isBackgroundInit()) { + Executor executor = getBootstrapExecutor(); + if (executor != null) { + String[] dependsOn = mbd.getDependsOn(); + if (dependsOn != null) { + for (String dep : dependsOn) { + getBean(dep); + } + } + CompletableFuture future = CompletableFuture.runAsync( + () -> instantiateSingletonInBackgroundThread(beanName), executor); + addSingletonFactory(beanName, () -> { + try { + future.join(); + } + catch (CompletionException ex) { + ReflectionUtils.rethrowRuntimeException(ex.getCause()); + } + return future; // not to be exposed, just to lead to ClassCastException in case of mismatch + }); + return (!mbd.isLazyInit() ? future : null); + } + else if (logger.isInfoEnabled()) { + logger.info("Bean '" + beanName + "' marked for background initialization " + + "without bootstrap executor configured - falling back to mainline initialization"); + } + } + + if (!mbd.isLazyInit()) { + try { + instantiateSingleton(beanName); + } + catch (BeanCurrentlyInCreationException ex) { + logger.info("Bean '" + beanName + "' marked for pre-instantiation (not lazy-init) " + + "but currently initialized by other thread - skipping it in mainline thread"); + } + } + return null; + } + + private void instantiateSingletonInBackgroundThread(String beanName) { + this.preInstantiationThread.set(PreInstantiation.BACKGROUND); + try { + instantiateSingleton(beanName); + } + catch (RuntimeException | Error ex) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to instantiate singleton bean '" + beanName + "' in background thread", ex); + } + throw ex; + } + finally { + this.preInstantiationThread.remove(); + } + } + + private void instantiateSingleton(String beanName) { + if (isFactoryBean(beanName)) { + Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); + if (bean instanceof SmartFactoryBean smartFactoryBean && smartFactoryBean.isEagerInit()) { + getBean(beanName); + } + } + else { + getBean(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 @@ -1014,27 +1236,8 @@ public void registerBeanDefinition(String beanName, BeanDefinition beanDefinitio if (!isBeanDefinitionOverridable(beanName)) { throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition); } - else if (existingDefinition.getRole() < beanDefinition.getRole()) { - // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE - if (logger.isInfoEnabled()) { - logger.info("Overriding user-defined bean definition for bean '" + beanName + - "' with a framework-generated bean definition: replacing [" + - existingDefinition + "] with [" + beanDefinition + "]"); - } - } - else if (!beanDefinition.equals(existingDefinition)) { - if (logger.isDebugEnabled()) { - logger.debug("Overriding bean definition for bean '" + beanName + - "' with a different definition: replacing [" + existingDefinition + - "] with [" + beanDefinition + "]"); - } - } else { - if (logger.isTraceEnabled()) { - logger.trace("Overriding bean definition for bean '" + beanName + - "' with an equivalent definition: replacing [" + existingDefinition + - "] with [" + beanDefinition + "]"); - } + logBeanDefinitionOverriding(beanName, beanDefinition, existingDefinition); } this.beanDefinitionMap.put(beanName, beanDefinition); } @@ -1053,6 +1256,11 @@ else if (!beanDefinition.equals(existingDefinition)) { } } else { + if (logger.isInfoEnabled()) { + logger.info("Removing alias '" + beanName + "' for bean '" + aliasedName + + "' due to registration of bean definition for bean '" + beanName + "': [" + + beanDefinition + "]"); + } removeAlias(beanName); } } @@ -1082,6 +1290,49 @@ else if (!beanDefinition.equals(existingDefinition)) { else if (isConfigurationFrozen()) { clearByTypeCache(); } + + // Cache a primary marker for the given bean. + if (beanDefinition.isPrimary()) { + this.primaryBeanNames.add(beanName); + } + } + + private void logBeanDefinitionOverriding(String beanName, BeanDefinition beanDefinition, + BeanDefinition existingDefinition) { + + boolean explicitBeanOverride = (this.allowBeanDefinitionOverriding != null); + if (existingDefinition.getRole() < beanDefinition.getRole()) { + // for example, was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE + if (logger.isInfoEnabled()) { + logger.info("Overriding user-defined bean definition for bean '" + beanName + + "' with a framework-generated bean definition: replacing [" + + existingDefinition + "] with [" + beanDefinition + "]"); + } + } + else if (!beanDefinition.equals(existingDefinition)) { + if (explicitBeanOverride && logger.isInfoEnabled()) { + logger.info("Overriding bean definition for bean '" + beanName + + "' with a different definition: replacing [" + existingDefinition + + "] with [" + beanDefinition + "]"); + } + if (logger.isDebugEnabled()) { + logger.debug("Overriding bean definition for bean '" + beanName + + "' with a different definition: replacing [" + existingDefinition + + "] with [" + beanDefinition + "]"); + } + } + else { + if (explicitBeanOverride && logger.isInfoEnabled()) { + logger.info("Overriding bean definition for bean '" + beanName + + "' with an equivalent definition: replacing [" + existingDefinition + + "] with [" + beanDefinition + "]"); + } + if (logger.isTraceEnabled()) { + logger.trace("Overriding bean definition for bean '" + beanName + + "' with an equivalent definition: replacing [" + existingDefinition + + "] with [" + beanDefinition + "]"); + } + } } @Override @@ -1130,9 +1381,12 @@ protected void resetBeanDefinition(String beanName) { // Remove corresponding bean from singleton cache, if any. Shouldn't usually // be necessary, rather just meant for overriding a context's default beans - // (e.g. the default StaticMessageSource in a StaticApplicationContext). + // (for example, the default StaticMessageSource in a StaticApplicationContext). destroySingleton(beanName); + // Remove a cached primary marker for the given bean. + this.primaryBeanNames.remove(beanName); + // Notify all post-processors that the specified bean definition has been reset. for (MergedBeanDefinitionPostProcessor processor : getBeanPostProcessorCache().mergedDefinition) { processor.resetBeanDefinition(beanName); @@ -1259,9 +1513,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); @@ -1296,6 +1549,9 @@ else if (candidateNames.length > 1) { if (candidateName == null) { candidateName = determineHighestPriorityCandidate(candidates, requiredType.toClass()); } + if (candidateName == null) { + candidateName = determineDefaultCandidate(candidates); + } if (candidateName != null) { Object beanInstance = candidates.get(candidateName); if (beanInstance == null) { @@ -1314,9 +1570,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) { @@ -1326,8 +1581,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()); @@ -1338,26 +1592,26 @@ else if (ObjectFactory.class == descriptor.getDependencyType() || ObjectProvider.class == descriptor.getDependencyType()) { return new DependencyObjectProvider(descriptor, requestingBeanName); } - else if (javaxInjectProviderClass == descriptor.getDependencyType()) { + else if (jakartaInjectProviderClass == descriptor.getDependencyType()) { return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName); } - else { + else if (descriptor.supportsLazyResolution()) { Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary( descriptor, requestingBeanName); - if (result == null) { - result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); + if (result != null) { + return result; } - return result; } + return doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); } - @Nullable - 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); try { - // Step 1: pre-resolved shortcut for single bean match, e.g. from @Autowired + // Step 1: pre-resolved shortcut for single bean match, for example, from @Autowired Object shortcut = descriptor.resolveShortcut(this); if (shortcut != null) { return shortcut; @@ -1365,7 +1619,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str Class type = descriptor.getDependencyType(); - // Step 2: pre-defined value or expression, e.g. from @Value + // Step 2: pre-defined value or expression, for example, from @Value Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); if (value != null) { if (value instanceof String strValue) { @@ -1386,15 +1640,36 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str } } - // Step 3a: multiple beans as stream / array / standard collection / plain map + // Step 3: shortcut for declared dependency name or qualifier-suggested name matching target bean name + if (descriptor.usesStandardBeanLookup()) { + String dependencyName = descriptor.getDependencyName(); + if (dependencyName == null || !containsBean(dependencyName)) { + String suggestedName = getAutowireCandidateResolver().getSuggestedName(descriptor); + dependencyName = (suggestedName != null && containsBean(suggestedName) ? suggestedName : null); + } + if (dependencyName != null) { + dependencyName = canonicalName(dependencyName); // dependency name can be alias of target name + if (isTypeMatch(dependencyName, type) && isAutowireCandidate(dependencyName, descriptor) && + !isFallback(dependencyName) && !hasPrimaryConflict(dependencyName, type) && + !isSelfReference(beanName, dependencyName)) { + if (autowiredBeanNames != null) { + autowiredBeanNames.add(dependencyName); + } + Object dependencyBean = getBean(dependencyName); + return resolveInstance(dependencyBean, descriptor, type, dependencyName); + } + } + } + + // Step 4a: multiple beans as stream / array / standard collection / plain map Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter); if (multipleBeans != null) { return multipleBeans; } - // Step 3b: direct bean matches, possibly direct beans of type Collection / Map + // Step 4b: direct bean matches, possibly direct beans of type Collection / Map Map matchingBeans = findAutowireCandidates(beanName, type, descriptor); if (matchingBeans.isEmpty()) { - // Step 3c (fallback): custom Collection / Map declarations for collecting multiple beans + // Step 4c (fallback): custom Collection / Map declarations for collecting multiple beans multipleBeans = resolveMultipleBeansFallback(descriptor, beanName, autowiredBeanNames, typeConverter); if (multipleBeans != null) { return multipleBeans; @@ -1409,7 +1684,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str String autowiredBeanName; Object instanceCandidate; - // Step 4: determine single candidate + // Step 5: determine single candidate if (matchingBeans.size() > 1) { autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); if (autowiredBeanName == null) { @@ -1433,33 +1708,37 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str instanceCandidate = entry.getValue(); } - // Step 5: validate single result + // Step 6: validate single result if (autowiredBeanNames != null) { autowiredBeanNames.add(autowiredBeanName); } if (instanceCandidate instanceof Class) { instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this); } - Object result = instanceCandidate; - if (result instanceof NullBean) { - if (isRequired(descriptor)) { - // Raise exception if null encountered for required injection point - raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor); - } - result = null; - } - if (!ClassUtils.isAssignableValue(type, result)) { - throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass()); - } - return result; + return resolveInstance(instanceCandidate, descriptor, type, autowiredBeanName); } finally { ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint); } } - @Nullable - private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName, + 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 + if (isRequired(descriptor)) { + raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor); + } + result = null; + } + if (!ClassUtils.isAssignableValue(type, result)) { + throw new BeanNotOfRequiredTypeException(name, type, candidate.getClass()); + } + return result; + + } + + private @Nullable Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { Class type = descriptor.getDependencyType(); @@ -1515,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(); @@ -1530,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(); @@ -1557,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(); @@ -1591,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( @@ -1689,8 +1964,8 @@ private void addCandidateEntry(Map candidates, String candidateN candidates.put(candidateName, beanInstance); } } - else if (containsSingleton(candidateName) || (descriptor instanceof StreamDependencyDescriptor streamDescriptor && - streamDescriptor.isOrdered())) { + 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)); } @@ -1707,23 +1982,46 @@ else if (containsSingleton(candidateName) || (descriptor instanceof StreamDepend * @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); if (primaryCandidate != null) { return primaryCandidate; } + // Step 2a: match bean name against declared dependency name + String dependencyName = descriptor.getDependencyName(); + if (dependencyName != null) { + for (String beanName : candidates.keySet()) { + if (matchesBeanName(beanName, dependencyName)) { + return beanName; + } + } + } + // Step 2b: match bean name against qualifier-suggested name + String suggestedName = getAutowireCandidateResolver().getSuggestedName(descriptor); + if (suggestedName != null) { + for (String beanName : candidates.keySet()) { + if (matchesBeanName(beanName, suggestedName)) { + return beanName; + } + } + } + // Step 3: check highest priority candidate String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType); if (priorityCandidate != null) { return priorityCandidate; } - // Fallback: pick directly registered dependency or qualified bean name match + // Step 4: pick unique default-candidate + String defaultCandidate = determineDefaultCandidate(candidates); + if (defaultCandidate != null) { + return defaultCandidate; + } + // Step 5: pick directly registered dependency for (Map.Entry entry : candidates.entrySet()) { String candidateName = entry.getKey(); Object beanInstance = entry.getValue(); - if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) || - matchesBeanName(candidateName, descriptor.getDependencyName())) { + if (beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) { return candidateName; } } @@ -1738,9 +2036,9 @@ 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()) { String candidateBeanName = entry.getKey(); Object beanInstance = entry.getValue(); @@ -1748,7 +2046,7 @@ protected String determinePrimaryCandidate(Map candidates, Class if (primaryBeanName != null) { boolean candidateLocal = containsBeanDefinition(candidateBeanName); boolean primaryLocal = containsBeanDefinition(primaryBeanName); - if (candidateLocal && primaryLocal) { + if (candidateLocal == primaryLocal) { throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), "more than one 'primary' bean found among candidates: " + candidates.keySet()); } @@ -1761,6 +2059,17 @@ else if (candidateLocal) { } } } + // Second pass: identify unique non-fallback candidate + if (primaryBeanName == null) { + for (String candidateBeanName : candidates.keySet()) { + if (!isFallback(candidateBeanName)) { + if (primaryBeanName != null) { + return null; + } + primaryBeanName = candidateBeanName; + } + } + } return primaryBeanName; } @@ -1774,12 +2083,14 @@ else if (candidateLocal) { * @param requiredType the target dependency type to match against * @return the name of the candidate with the highest priority, * or {@code null} if none found + * @throws NoUniqueBeanDefinitionException if multiple beans are detected with + * 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; for (Map.Entry entry : candidates.entrySet()) { String candidateBeanName = entry.getKey(); Object beanInstance = entry.getValue(); @@ -1788,13 +2099,12 @@ protected String determineHighestPriorityCandidate(Map candidate if (candidatePriority != null) { if (highestPriority != null) { if (candidatePriority.equals(highestPriority)) { - throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), - "Multiple beans found with the same priority ('" + highestPriority + - "') among candidates: " + candidates.keySet()); + highestPriorityConflictDetected = true; } else if (candidatePriority < highestPriority) { highestPriorityBeanName = candidateBeanName; highestPriority = candidatePriority; + highestPriorityConflictDetected = false; } } else { @@ -1804,6 +2114,13 @@ else if (candidatePriority < highestPriority) { } } } + + if (highestPriorityConflictDetected) { + throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), + "Multiple beans found with the same highest priority (" + highestPriority + + ") among candidates: " + candidates.keySet()); + + } return highestPriorityBeanName; } @@ -1811,7 +2128,7 @@ else if (candidatePriority < highestPriority) { * Return whether the bean definition for the given bean name has been * marked as a primary bean. * @param beanName the name of the bean - * @param beanInstance the corresponding bean instance (can be null) + * @param beanInstance the corresponding bean instance (can be {@code null}) * @return whether the given bean qualifies as primary */ protected boolean isPrimary(String beanName, Object beanInstance) { @@ -1823,6 +2140,21 @@ protected boolean isPrimary(String beanName, Object beanInstance) { parent.isPrimary(transformedBeanName, beanInstance)); } + /** + * Return whether the bean definition for the given bean name has been + * marked as a fallback bean. + * @param beanName the name of the bean + * @since 6.2 + */ + private boolean isFallback(String beanName) { + String transformedBeanName = transformedBeanName(beanName); + if (containsBeanDefinition(transformedBeanName)) { + return getMergedLocalBeanDefinition(transformedBeanName).isFallback(); + } + return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent && + parent.isFallback(transformedBeanName)); + } + /** * Return the priority assigned for the given bean instance by * the {@code jakarta.annotation.Priority} annotation. @@ -1835,8 +2167,7 @@ protected boolean isPrimary(String beanName, Object beanInstance) { * @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); @@ -1844,6 +2175,28 @@ protected Integer getPriority(Object beanInstance) { return null; } + /** + * Return a unique "default-candidate" among remaining non-default candidates. + * @param candidates a Map of candidate names and candidate instances + * (or candidate classes if not created yet) that match the required type + * @return the name of the default candidate, or {@code null} if none found + * @since 6.2.4 + * @see AbstractBeanDefinition#isDefaultCandidate() + */ + @Nullable + private String determineDefaultCandidate(Map candidates) { + String defaultBeanName = null; + for (String candidateBeanName : candidates.keySet()) { + if (AutowireUtils.isDefaultCandidate(this, candidateBeanName)) { + if (defaultBeanName != null) { + return null; + } + defaultBeanName = candidateBeanName; + } + } + return defaultBeanName; + } + /** * Determine whether the given candidate name matches the bean name or the aliases * stored in this bean definition. @@ -1858,12 +2211,27 @@ 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;") private boolean isSelfReference(@Nullable String beanName, @Nullable String candidateName) { return (beanName != null && candidateName != null && (beanName.equals(candidateName) || (containsBeanDefinition(candidateName) && beanName.equals(getMergedLocalBeanDefinition(candidateName).getFactoryBeanName())))); } + /** + * Determine whether there is a primary bean registered for the given dependency type, + * not matching the given bean name. + */ + private boolean hasPrimaryConflict(String beanName, Class dependencyType) { + for (String candidate : this.primaryBeanNames) { + if (isTypeMatch(candidate, dependencyType) && !candidate.equals(beanName)) { + return true; + } + } + return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent && + parent.hasPrimaryConflict(beanName, dependencyType)); + } + /** * Raise a NoSuchBeanDefinitionException or BeanNotOfRequiredTypeException * for an unresolvable dependency. @@ -1912,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 @@ -1924,6 +2292,10 @@ public Object resolveCandidate(String beanName, Class requiredType, BeanFacto return (!ObjectUtils.isEmpty(args) ? beanFactory.getBean(beanName, args) : super.resolveCandidate(beanName, requiredType, beanFactory)); } + @Override + public boolean usesStandardBeanLookup() { + return ObjectUtils.isEmpty(args); + } }; Object result = doResolveDependency(descriptorToUse, beanName, null, null); return (result instanceof Optional optional ? optional : Optional.ofNullable(result)); @@ -2005,6 +2377,11 @@ public NestedDependencyDescriptor(DependencyDescriptor original) { super(original); increaseNestingLevel(); } + + @Override + public boolean usesStandardBeanLookup() { + return true; + } } @@ -2050,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); @@ -2074,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); } @@ -2094,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); @@ -2106,6 +2481,10 @@ public Object getIfAvailable() throws BeansException { public boolean isRequired() { return false; } + @Override + public boolean usesStandardBeanLookup() { + return true; + } }; return doResolveDependency(descriptorToUse, this.beanName, null, null); } @@ -2130,16 +2509,18 @@ 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() { return false; } @Override - @Nullable - public Object resolveNotUnique(ResolvableType type, Map matchingBeans) { + public boolean usesStandardBeanLookup() { + return true; + } + @Override + public @Nullable Object resolveNotUnique(ResolvableType type, Map matchingBeans) { return null; } }; @@ -2170,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); } @@ -2196,6 +2576,34 @@ private Stream resolveStream(boolean ordered) { Object result = doResolveDependency(descriptorToUse, this.beanName, null, null); return (result instanceof Stream stream ? stream : Stream.of(result)); } + + @Override + 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)) + .filter(bean -> !(bean instanceof NullBean)); + } + + @Override + public Stream orderedStream(Predicate> customFilter, boolean includeNonSingletons) { + String[] beanNames = beanNamesForStream(this.descriptor.getResolvableType(), includeNonSingletons, true); + if (beanNames.length == 0) { + return Stream.empty(); + } + Map matchingBeans = CollectionUtils.newLinkedHashMap(beanNames.length); + for (String beanName : beanNames) { + if (AutowireUtils.isAutowireCandidate(DefaultListableBeanFactory.this, beanName) && + customFilter.test(getType(beanName))) { + Object beanInstance = getBean(beanName); + if (!(beanInstance instanceof NullBean)) { + matchingBeans.put(beanName, beanInstance); + } + } + } + return matchingBeans.values().stream().sorted(adaptOrderComparator(matchingBeans)); + } } @@ -2217,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(); } } @@ -2230,7 +2637,9 @@ public Object get() throws BeansException { * that is aware of the bean metadata of the instances to sort. *

    Lookup for the method factory of an instance to sort, if any, and let the * comparator retrieve the {@link org.springframework.core.annotation.Order} - * value defined on it. This essentially allows for the following construct: + * value defined on it. + *

    As of 6.1.2, this class takes the {@link AbstractBeanDefinition#ORDER_ATTRIBUTE} + * attribute into account. */ private class FactoryAwareOrderSourceProvider implements OrderComparator.OrderSourceProvider { @@ -2241,15 +2650,24 @@ 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; } try { RootBeanDefinition beanDefinition = (RootBeanDefinition) getMergedBeanDefinition(beanName); - List sources = new ArrayList<>(2); + List sources = new ArrayList<>(3); + Object orderAttribute = beanDefinition.getAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE); + if (orderAttribute != null) { + if (orderAttribute instanceof Integer order) { + sources.add((Ordered) () -> order); + } + else { + throw new IllegalStateException("Invalid value type for attribute '" + + AbstractBeanDefinition.ORDER_ATTRIBUTE + "': " + orderAttribute.getClass().getName()); + } + } Method factoryMethod = beanDefinition.getResolvedFactoryMethod(); if (factoryMethod != null) { sources.add(factoryMethod); @@ -2266,4 +2684,10 @@ public Object getOrderSource(Object obj) { } } + + private enum PreInstantiation { + + MAIN, BACKGROUND + } + } 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 9b189b34312d..69818786e6dd 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,12 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +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; @@ -33,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; @@ -74,32 +79,50 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100; + /** Common lock for singleton creation. */ + final Lock singletonLock = new ReentrantLock(); + /** Cache of singleton objects: bean name to bean instance. */ private final Map singletonObjects = new ConcurrentHashMap<>(256); - /** Cache of singleton factories: bean name to ObjectFactory. */ - private final Map> singletonFactories = new HashMap<>(16); + /** Creation-time registry of singleton factories: bean name to ObjectFactory. */ + private final Map> singletonFactories = new ConcurrentHashMap<>(16); + + /** Custom callbacks for singleton creation/registration. */ + private final Map> singletonCallbacks = new ConcurrentHashMap<>(16); /** Cache of early singleton objects: bean name to bean instance. */ private final Map earlySingletonObjects = new ConcurrentHashMap<>(16); /** Set of registered singletons, containing the bean names in registration order. */ - private final Set registeredSingletons = new LinkedHashSet<>(256); + private final Set registeredSingletons = Collections.synchronizedSet(new LinkedHashSet<>(256)); /** Names of beans that are currently in creation. */ - private final Set singletonsCurrentlyInCreation = - Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + private final Set singletonsCurrentlyInCreation = ConcurrentHashMap.newKeySet(16); /** Names of beans currently excluded from in creation checks. */ - private final Set inCreationCheckExclusions = - Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + private final Set inCreationCheckExclusions = ConcurrentHashMap.newKeySet(16); - /** Collection of suppressed Exceptions, available for associating related causes. */ - @Nullable - private Set suppressedExceptions; + /** Specific lock for lenient creation tracking. */ + private final Lock lenientCreationLock = new ReentrantLock(); + + /** Specific lock condition for lenient creation tracking. */ + private final Condition lenientCreationFinished = this.lenientCreationLock.newCondition(); + + /** 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 boolean singletonsCurrentlyInDestruction = false; + private volatile boolean singletonsCurrentlyInDestruction = false; + + /** Collection of suppressed Exceptions, available for associating related causes. */ + private @Nullable Set suppressedExceptions; /** Disposable bean instances: bean name to disposable instance. */ private final Map disposableBeans = new LinkedHashMap<>(); @@ -118,53 +141,59 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException { Assert.notNull(beanName, "Bean name must not be null"); Assert.notNull(singletonObject, "Singleton object must not be null"); - synchronized (this.singletonObjects) { - Object oldObject = this.singletonObjects.get(beanName); - if (oldObject != null) { - throw new IllegalStateException("Could not register object [" + singletonObject + - "] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound"); - } + this.singletonLock.lock(); + try { addSingleton(beanName, singletonObject); } + finally { + this.singletonLock.unlock(); + } } /** - * Add the given singleton object to the singleton cache of this factory. - *

    To be called for eager registration of singletons. + * Add the given singleton object to the singleton registry. + *

    To be called for exposure of freshly registered/created singletons. * @param beanName the name of the bean * @param singletonObject the singleton object */ protected void addSingleton(String beanName, Object singletonObject) { - synchronized (this.singletonObjects) { - this.singletonObjects.put(beanName, singletonObject); - this.singletonFactories.remove(beanName); - this.earlySingletonObjects.remove(beanName); - this.registeredSingletons.add(beanName); + Object oldObject = this.singletonObjects.putIfAbsent(beanName, singletonObject); + if (oldObject != null) { + throw new IllegalStateException("Could not register object [" + singletonObject + + "] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound"); + } + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + + Consumer callback = this.singletonCallbacks.get(beanName); + if (callback != null) { + callback.accept(singletonObject); } } /** * Add the given singleton factory for building the specified singleton * if necessary. - *

    To be called for eager registration of singletons, e.g. to be able to + *

    To be called for early exposure purposes, for example, to be able to * resolve circular references. * @param beanName the name of the bean * @param singletonFactory the factory for the singleton object */ protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); - synchronized (this.singletonObjects) { - if (!this.singletonObjects.containsKey(beanName)) { - this.singletonFactories.put(beanName, singletonFactory); - this.earlySingletonObjects.remove(beanName); - this.registeredSingletons.add(beanName); - } - } + this.singletonFactories.put(beanName, singletonFactory); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } + + @Override + public void addSingletonCallback(String beanName, Consumer singletonConsumer) { + this.singletonCallbacks.put(beanName, singletonConsumer); } @Override - @Nullable - public Object getSingleton(String beanName) { + public @Nullable Object getSingleton(String beanName) { return getSingleton(beanName, true); } @@ -176,15 +205,18 @@ 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) { - // Quick check for existing instance without full singleton lock + 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)) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { - synchronized (this.singletonObjects) { - // Consistent creation of early reference within full singleton lock + if (!this.singletonLock.tryLock()) { + // Avoid early singleton inference outside of original creation thread. + return null; + } + try { + // Consistent creation of early reference within full singleton lock. singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { singletonObject = this.earlySingletonObjects.get(beanName); @@ -192,12 +224,20 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { ObjectFactory singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); - this.earlySingletonObjects.put(beanName, singletonObject); - this.singletonFactories.remove(beanName); + // Singleton could have been added or removed in the meantime. + if (this.singletonFactories.remove(beanName) != null) { + this.earlySingletonObjects.put(beanName, singletonObject); + } + else { + singletonObject = this.singletonObjects.get(beanName); + } } } } } + finally { + this.singletonLock.unlock(); + } } } return singletonObject; @@ -211,11 +251,50 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { * with, if necessary * @return the registered singleton object */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Object getSingleton(String beanName, ObjectFactory singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); - synchronized (this.singletonObjects) { + + 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) { + if (acquireLock && !locked) { + if (Boolean.TRUE.equals(lockFlag)) { + // Another thread is busy in a singleton factory callback, potentially blocked. + // 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("Obtaining singleton bean '" + beanName + "' in thread \"" + + Thread.currentThread().getName() + "\" while other thread holds " + + "singleton lock for other beans " + this.singletonsCurrentlyInCreation); + } + this.lenientCreationLock.lock(); + try { + this.singletonsInLenientCreation.add(beanName); + } + finally { + this.lenientCreationLock.unlock(); + } + } + else { + // No specific locking indication (outside a coordinated bootstrap) and + // singleton lock currently held by some other creation method -> wait. + this.singletonLock.lock(); + locked = true; + // Singleton object might have possibly appeared in the meantime. + singletonObject = this.singletonObjects.get(beanName); + if (singletonObject != null) { + return singletonObject; + } + } + } + if (this.singletonsCurrentlyInDestruction) { throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction " + @@ -224,15 +303,76 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { if (logger.isDebugEnabled()) { logger.debug("Creating shared instance of singleton bean '" + beanName + "'"); } - beforeSingletonCreation(beanName); + + try { + beforeSingletonCreation(beanName); + } + catch (BeanCurrentlyInCreationException ex) { + 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) { + currentThread.interrupt(); + } + finally { + if (otherThread != null) { + this.lenientWaitingThreads.remove(currentThread); + } + } + } + } + finally { + this.lenientCreationLock.unlock(); + } + if (singletonObject != null) { + return singletonObject; + } + if (locked) { + throw ex; + } + // Try late locking for waiting on specific bean to be finished. + this.singletonLock.lock(); + locked = true; + // Lock-created singleton object should have appeared in the meantime. + singletonObject = this.singletonObjects.get(beanName); + if (singletonObject != null) { + return singletonObject; + } + beforeSingletonCreation(beanName); + } + boolean newSingleton = false; - boolean recordSuppressedExceptions = (this.suppressedExceptions == null); + boolean recordSuppressedExceptions = (locked && this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = new LinkedHashSet<>(); } try { - singletonObject = singletonFactory.getObject(); - newSingleton = true; + // Leniently created singleton object could have appeared in the meantime. + singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { + this.currentCreationThreads.put(beanName, currentThread); + try { + singletonObject = singletonFactory.getObject(); + } + finally { + this.currentCreationThreads.remove(beanName); + } + newSingleton = true; + } } catch (IllegalStateException ex) { // Has the singleton object implicitly appeared in the meantime -> @@ -256,17 +396,70 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { } afterSingletonCreation(beanName); } + if (newSingleton) { - addSingleton(beanName, singletonObject); + try { + addSingleton(beanName, singletonObject); + } + catch (IllegalStateException ex) { + // Leniently accept same instance if implicitly appeared. + Object object = this.singletonObjects.get(beanName); + if (singletonObject != object) { + throw ex; + } + } } } return singletonObject; } + finally { + if (locked) { + this.singletonLock.unlock(); + } + this.lenientCreationLock.lock(); + try { + this.singletonsInLenientCreation.remove(beanName); + this.lenientWaitingThreads.entrySet().removeIf( + entry -> entry.getValue() == currentThread); + this.lenientCreationFinished.signalAll(); + } + finally { + this.lenientCreationLock.unlock(); + } + } + } + + 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, all threads are forced to hold a full lock through {@code null}. + * {@link DefaultListableBeanFactory} overrides this to specifically handle its + * threads during the pre-instantiation phase: {@code true} for the main thread, + * {@code false} for managed background threads, and configuration-dependent + * behavior for unmanaged threads. + * @return {@code true} if the current thread is explicitly allowed to hold the + * lock but also accepts lenient fallback behavior, {@code false} if it is + * explicitly not allowed to hold the lock and therefore forced to use lenient + * fallback behavior, or {@code null} if there is no specific indication + * (traditional behavior: forced to always hold a full lock) + * @since 6.2 + */ + protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { + return null; } /** * Register an exception that happened to get suppressed during the creation of a - * singleton bean instance, e.g. a temporary circular reference resolution problem. + * singleton bean instance, for example, a temporary circular reference resolution problem. *

    The default implementation preserves any given exception in this registry's * collection of suppressed exceptions, up to a limit of 100 exceptions, adding * them as related causes to an eventual top-level {@link BeanCreationException}. @@ -274,26 +467,21 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { * @see BeanCreationException#getRelatedCauses() */ protected void onSuppressedException(Exception ex) { - synchronized (this.singletonObjects) { - if (this.suppressedExceptions != null && this.suppressedExceptions.size() < SUPPRESSED_EXCEPTIONS_LIMIT) { - this.suppressedExceptions.add(ex); - } + if (this.suppressedExceptions != null && this.suppressedExceptions.size() < SUPPRESSED_EXCEPTIONS_LIMIT) { + this.suppressedExceptions.add(ex); } } /** - * Remove the bean with the given name from the singleton cache of this factory, - * to be able to clean up eager registration of a singleton if creation failed. + * Remove the bean with the given name from the singleton registry, either on + * regular destruction or on cleanup after early exposure when creation failed. * @param beanName the name of the bean - * @see #getSingletonMutex() */ protected void removeSingleton(String beanName) { - synchronized (this.singletonObjects) { - this.singletonObjects.remove(beanName); - this.singletonFactories.remove(beanName); - this.earlySingletonObjects.remove(beanName); - this.registeredSingletons.remove(beanName); - } + this.singletonObjects.remove(beanName); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.remove(beanName); } @Override @@ -303,16 +491,12 @@ public boolean containsSingleton(String beanName) { @Override public String[] getSingletonNames() { - synchronized (this.singletonObjects) { - return StringUtils.toStringArray(this.registeredSingletons); - } + return StringUtils.toStringArray(this.registeredSingletons); } @Override public int getSingletonCount() { - synchronized (this.singletonObjects) { - return this.registeredSingletons.size(); - } + return this.registeredSingletons.size(); } @@ -386,7 +570,7 @@ public void registerDisposableBean(String beanName, DisposableBean bean) { /** * Register a containment relationship between two beans, - * e.g. between an inner bean and its containing outer bean. + * for example, between an inner bean and its containing outer bean. *

    Also registers the containing bean as dependent on the contained bean * in terms of destruction order. * @param containedBeanName the name of the contained (inner) bean @@ -508,9 +692,7 @@ public void destroySingletons() { if (logger.isTraceEnabled()) { logger.trace("Destroying singletons in " + this); } - synchronized (this.singletonObjects) { - this.singletonsCurrentlyInDestruction = true; - } + this.singletonsCurrentlyInDestruction = true; String[] disposableBeanNames; synchronized (this.disposableBeans) { @@ -524,7 +706,13 @@ public void destroySingletons() { this.dependentBeanMap.clear(); this.dependenciesForBeanMap.clear(); - clearSingletonCache(); + this.singletonLock.lock(); + try { + clearSingletonCache(); + } + finally { + this.singletonLock.unlock(); + } } /** @@ -532,13 +720,11 @@ public void destroySingletons() { * @since 4.3.15 */ protected void clearSingletonCache() { - synchronized (this.singletonObjects) { - this.singletonObjects.clear(); - this.singletonFactories.clear(); - this.earlySingletonObjects.clear(); - this.registeredSingletons.clear(); - this.singletonsCurrentlyInDestruction = false; - } + this.singletonObjects.clear(); + this.singletonFactories.clear(); + this.earlySingletonObjects.clear(); + this.registeredSingletons.clear(); + this.singletonsCurrentlyInDestruction = false; } /** @@ -548,15 +734,35 @@ protected void clearSingletonCache() { * @see #destroyBean */ public void destroySingleton(String beanName) { - // Remove a registered singleton of the given name, if any. - removeSingleton(beanName); - // Destroy the corresponding DisposableBean instance. + // This also triggers the destruction of dependent beans. DisposableBean disposableBean; synchronized (this.disposableBeans) { disposableBean = this.disposableBeans.remove(beanName); } destroyBean(beanName, disposableBean); + + // destroySingletons() removes all singleton instances at the end, + // leniently tolerating late retrieval during the shutdown phase. + if (!this.singletonsCurrentlyInDestruction) { + // 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. + 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); + } + else { + this.singletonLock.lock(); + try { + removeSingleton(beanName); + } + finally { + this.singletonLock.unlock(); + } + } + } } /** @@ -567,16 +773,16 @@ public void destroySingleton(String beanName) { */ protected void destroyBean(String beanName, @Nullable DisposableBean bean) { // Trigger destruction of dependent beans first... - Set dependencies; + Set dependentBeanNames; synchronized (this.dependentBeanMap) { // Within full synchronization in order to guarantee a disconnected Set - dependencies = this.dependentBeanMap.remove(beanName); + dependentBeanNames = this.dependentBeanMap.remove(beanName); } - if (dependencies != null) { + if (dependentBeanNames != null) { if (logger.isTraceEnabled()) { - logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependencies); + logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependentBeanNames); } - for (String dependentBeanName : dependencies) { + for (String dependentBeanName : dependentBeanNames) { destroySingleton(dependentBeanName); } } @@ -621,16 +827,10 @@ protected void destroyBean(String beanName, @Nullable DisposableBean bean) { this.dependenciesForBeanMap.remove(beanName); } - /** - * Exposes the singleton mutex to subclasses and external collaborators. - *

    Subclasses should synchronize on the given Object if they perform - * any sort of extended singleton creation phase. In particular, subclasses - * should not have their own mutexes involved in singleton creation, - * to avoid the potential for deadlocks in lazy-init situations. - */ + @Deprecated(since = "6.2") @Override public final Object getSingletonMutex() { - return this.singletonObjects; + return new Object(); } } 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 ea07ba7bcbcd..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 @@ -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.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; /** @@ -147,7 +144,7 @@ else if (paramTypes.length == 1 && boolean.class != paramTypes[0]) { beanName + "' has a non-boolean parameter - not supported as destroy method"); } } - destroyMethod = ClassUtils.getInterfaceMethodIfPossible(destroyMethod, bean.getClass()); + destroyMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(destroyMethod, bean.getClass()); destroyMethods.add(destroyMethod); } } @@ -177,7 +174,7 @@ public DisposableBeanAdapter(Object bean, List postProcessors) { this.bean = bean; @@ -253,16 +250,15 @@ else if (this.destroyMethodNames != null) { for (String destroyMethodName : this.destroyMethodNames) { Method destroyMethod = determineDestroyMethod(destroyMethodName); if (destroyMethod != null) { - invokeCustomDestroyMethod( - ClassUtils.getInterfaceMethodIfPossible(destroyMethod, this.bean.getClass())); + destroyMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(destroyMethod, this.bean.getClass()); + invokeCustomDestroyMethod(destroyMethod); } } } } - @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); @@ -272,7 +268,7 @@ private Method determineDestroyMethod(String destroyMethodName) { if (destroyMethod != null) { return destroyMethod; } - for (Class beanInterface : beanClass.getInterfaces()) { + for (Class beanInterface : ClassUtils.getAllInterfacesForClass(beanClass)) { destroyMethod = findDestroyMethod(beanInterface, methodName); if (destroyMethod != null) { return destroyMethod; @@ -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)); @@ -342,7 +337,7 @@ else if (!reactiveStreamsPresent || !new ReactiveDestroyMethodHandler().await(de } } - void logDestroyMethodException(Method destroyMethod, Throwable ex) { + void logDestroyMethodException(Method destroyMethod, @Nullable Throwable ex) { if (logger.isWarnEnabled()) { String msg = "Custom destroy method '" + destroyMethod.getName() + "' on bean with name '" + this.beanName + "' propagated an exception"; @@ -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 bd19a2f4fc41..da0199f0d1e4 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-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.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,12 +117,13 @@ protected Object getCachedObjectForFactoryBean(String beanName) { */ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName, boolean shouldPostProcess) { if (factory.isSingleton() && containsSingleton(beanName)) { - synchronized (getSingletonMutex()) { + this.singletonLock.lock(); + try { Object object = this.factoryBeanObjectCache.get(beanName); if (object == null) { object = doGetObjectFromFactoryBean(factory, beanName); // Only post-process and store if not put there already during getObject() call above - // (e.g. because of circular reference processing triggered by custom getBean calls) + // (for example, because of circular reference processing triggered by custom getBean calls) Object alreadyThere = this.factoryBeanObjectCache.get(beanName); if (alreadyThere != null) { object = alreadyThere; @@ -131,7 +131,7 @@ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanNam else { if (shouldPostProcess) { if (isSingletonCurrentlyInCreation(beanName)) { - // Temporarily return non-post-processed object, not storing it yet.. + // Temporarily return non-post-processed object, not storing it yet return object; } beforeSingletonCreation(beanName); @@ -153,6 +153,9 @@ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanNam } return object; } + finally { + this.singletonLock.unlock(); + } } else { Object object = doGetObjectFromFactoryBean(factory, beanName); @@ -234,10 +237,8 @@ protected FactoryBean getFactoryBean(String beanName, Object beanInstance) th */ @Override protected void removeSingleton(String beanName) { - synchronized (getSingletonMutex()) { - super.removeSingleton(beanName); - this.factoryBeanObjectCache.remove(beanName); - } + super.removeSingleton(beanName); + this.factoryBeanObjectCache.remove(beanName); } /** @@ -245,10 +246,8 @@ protected void removeSingleton(String beanName) { */ @Override protected void clearSingletonCache() { - synchronized (getSingletonMutex()) { - super.clearSingletonCache(); - this.factoryBeanObjectCache.clear(); - } + super.clearSingletonCache(); + this.factoryBeanObjectCache.clear(); } } 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 8381b7b20f24..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; /** @@ -27,7 +28,7 @@ * parent bean definition can be flexibly configured through the "parentName" property. * *

    In general, use this {@code GenericBeanDefinition} class for the purpose of - * registering declarative bean definitions (e.g. XML definitions which a bean + * registering declarative bean definitions (for example, XML definitions which a bean * post-processor might operate on, potentially even reconfiguring the parent name). * Use {@code RootBeanDefinition}/{@code ChildBeanDefinition} where parent/child * relationships happen to be pre-determined, and prefer {@link RootBeanDefinition} @@ -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 3a3c84b11a1a..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 @@ -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.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,13 +29,12 @@ 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; /** * Basic {@link AutowireCandidateResolver} that performs a full generic type * match with the candidate's type if the dependency is declared as a generic type - * (e.g. {@code Repository}). + * (for example, {@code Repository}). * *

    This is the base class for * {@link org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver}, @@ -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,6 +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") // Dataflow analysis limitation protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) { ResolvableType dependencyType = descriptor.getResolvableType(); if (dependencyType.getType() instanceof Class) { @@ -102,22 +102,6 @@ protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, Dependenc } } } - else { - // Pre-existing target type: In case of a generic FactoryBean type, - // unwrap nested generic type when matching a non-FactoryBean type. - Class resolvedClass = targetType.resolve(); - if (resolvedClass != null && FactoryBean.class.isAssignableFrom(resolvedClass)) { - Class typeToBeMatched = dependencyType.resolve(); - if (typeToBeMatched != null && !FactoryBean.class.isAssignableFrom(typeToBeMatched)) { - targetType = targetType.getGeneric(); - if (descriptor.fallbackMatchAllowed()) { - // Matching the Class-based type determination for FactoryBean - // objects in the lazy-determination getType code path below. - targetType = ResolvableType.forClass(targetType.resolve()); - } - } - } - } } if (targetType == null) { @@ -144,19 +128,39 @@ protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, Dependenc if (cacheType) { rbd.targetType = targetType; } - if (descriptor.fallbackMatchAllowed() && - (targetType.hasUnresolvableGenerics() || targetType.resolve() == Properties.class)) { - // Fallback matches allow unresolvable generics, e.g. plain HashMap to Map; + + // Pre-declared target type: In case of a generic FactoryBean type, + // unwrap nested generic type when matching a non-FactoryBean type. + Class targetClass = targetType.resolve(); + if (targetClass != null && FactoryBean.class.isAssignableFrom(targetClass)) { + Class classToMatch = dependencyType.resolve(); + if (classToMatch != null && !FactoryBean.class.isAssignableFrom(classToMatch) && + !classToMatch.isAssignableFrom(targetClass)) { + targetType = targetType.getGeneric(); + if (descriptor.fallbackMatchAllowed()) { + // Matching the Class-based type determination for FactoryBean + // objects in the lazy-determination getType code path above. + targetType = ResolvableType.forClass(targetType.resolve()); + } + } + } + + if (descriptor.fallbackMatchAllowed()) { + // Fallback matches allow unresolvable generics, for example, plain HashMap to Map; // and pragmatically also java.util.Properties to any Map (since despite formally being a // Map, java.util.Properties is usually perceived as a Map). - return true; + if (targetType.hasUnresolvableGenerics()) { + return dependencyType.isAssignableFromResolvedPart(targetType); + } + else if (targetType.resolve() == Properties.class) { + return true; + } } // Full check for complex generic type match... 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())) { @@ -169,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 8d930b357395..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,7 +83,7 @@ public V get(RegisteredBean registeredBean) throws Exception { return after.applyWithException(registeredBean, InstanceSupplier.this.get(registeredBean)); } @Override - public Method getFactoryMethod() { + public @Nullable Method getFactoryMethod() { return InstanceSupplier.this.getFactoryMethod(); } }; @@ -126,7 +126,7 @@ public T get(RegisteredBean registeredBean) throws Exception { return supplier.getWithException(); } @Override - 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 f51af88735c6..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 @@ -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,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,15 +42,13 @@ */ public class LookupOverride extends MethodOverride { - @Nullable - private final String beanName; + private final @Nullable String beanName; - @Nullable - private Method method; + private @Nullable Method method; /** - * Construct a new LookupOverride. + * Construct a new {@code LookupOverride}. * @param methodName the name of the method to override * @param beanName the name of the bean in the current {@code BeanFactory} that the * overridden method should return (may be {@code null} for type-based bean retrieval) @@ -60,7 +59,7 @@ public LookupOverride(String methodName, @Nullable String beanName) { } /** - * Construct a new LookupOverride. + * Construct a new {@code LookupOverride}. * @param method the method declaration to override * @param beanName the name of the bean in the current {@code BeanFactory} that the * overridden method should return (may be {@code null} for type-based bean retrieval) @@ -73,10 +72,9 @@ public LookupOverride(Method method, @Nullable String beanName) { /** - * Return the name of the bean that should be returned by this method. + * 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/MethodDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.java index c895734f0ec3..6670d5116f22 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodDescriptor.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,26 +16,27 @@ package org.springframework.beans.factory.support; +import java.lang.reflect.Method; + import org.springframework.util.ClassUtils; /** - * Descriptor for a {@link java.lang.reflect.Method Method} which holds a + * Descriptor for a {@link Method Method} which holds a * reference to the method's {@linkplain #declaringClass declaring class}, * {@linkplain #methodName name}, and {@linkplain #parameterTypes parameter types}. * + * @author Sam Brannen + * @since 6.0.11 * @param declaringClass the method's declaring class * @param methodName the name of the method * @param parameterTypes the types of parameters accepted by the method - * @author Sam Brannen - * @since 6.0.11 */ record MethodDescriptor(Class declaringClass, String methodName, Class... parameterTypes) { /** * Create a {@link MethodDescriptor} for the supplied bean class and method name. *

    The supplied {@code methodName} may be a {@linkplain Method#getName() - * simple method name} or a - * {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method) + * simple method name} or a {@linkplain ClassUtils#getQualifiedMethodName(Method) * qualified method name}. *

    If the method name is fully qualified, this utility will parse the * method name and its declaring class from the qualified method name and then 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 d250320dded4..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 @@ -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. @@ -17,9 +17,11 @@ package org.springframework.beans.factory.support; 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; @@ -41,8 +43,7 @@ public abstract class MethodOverride implements BeanMetadataElement { private boolean overloaded = true; - @Nullable - private Object source; + private @Nullable Object source; /** @@ -89,8 +90,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } @@ -107,13 +107,13 @@ public Object getSource() { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof MethodOverride that && - ObjectUtils.nullSafeEquals(this.methodName, that.methodName) && + this.methodName.equals(that.methodName) && ObjectUtils.nullSafeEquals(this.source, that.source))); } @Override public int hashCode() { - return ObjectUtils.nullSafeHash(this.methodName, this.source); + return Objects.hash(this.methodName, 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..5f1efdc2d064 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 @@ -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 @@ -90,8 +90,7 @@ public boolean isEmpty() { * @param method 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/NullBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/NullBean.java index 7905acc09554..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,11 +16,12 @@ 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, e.g. for a {@code null} value + * Internal representation of a null bean instance, for example, for a {@code null} value * returned from {@link FactoryBean#getObject()} or from a factory method. * *

    Each such null bean is represented by a dedicated {@code NullBean} instance 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 ebd2b6a1aa6f..40a65d4e70eb 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 @@ -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; @@ -128,7 +129,7 @@ public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader /** * Property suffix for references to other beans in the current - * BeanFactory: e.g. {@code owner.dog(ref)=fido}. + * BeanFactory: for example, {@code owner.dog(ref)=fido}. * Whether this is a reference to a singleton or a prototype * will depend on the definition of the target bean. */ @@ -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; @@ -165,7 +165,7 @@ public PropertiesBeanDefinitionReader(BeanDefinitionRegistry registry) { * Set the default parent bean for this bean factory. * If a child bean definition handled by this factory provides neither * a parent nor a class attribute, this default value gets used. - *

    Can be used e.g. for view definition files, to define a parent + *

    Can be used, for example, for view definition files, to define a parent * with a default view class and common attributes for all views. * View definitions that define their own parent or carry their own * class can still override this. @@ -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; } @@ -219,7 +218,7 @@ public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreExce /** * Load bean definitions from the specified properties file. * @param resource the resource descriptor for the properties file - * @param prefix a filter within the keys in the map: e.g. 'beans.' + * @param prefix a filter within the keys in the map: for example, 'beans.' * (can be empty or {@code null}) * @return the number of bean definitions found * @throws BeanDefinitionStoreException in case of loading or parsing errors @@ -243,7 +242,7 @@ public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefin * Load bean definitions from the specified properties file. * @param encodedResource the resource descriptor for the properties file, * allowing to specify an encoding to use for parsing the file - * @param prefix a filter within the keys in the map: e.g. 'beans.' + * @param prefix a filter within the keys in the map: for example, 'beans.' * (can be empty or {@code null}) * @return the number of bean definitions found * @throws BeanDefinitionStoreException in case of loading or parsing errors @@ -294,7 +293,7 @@ public int registerBeanDefinitions(ResourceBundle rb) throws BeanDefinitionStore *

    Similar syntax as for a Map. This method is useful to enable * standard Java internationalization support. * @param rb the ResourceBundle to load from - * @param prefix a filter within the keys in the map: e.g. 'beans.' + * @param prefix a filter within the keys in the map: for example, 'beans.' * (can be empty or {@code null}) * @return the number of bean definitions found * @throws BeanDefinitionStoreException in case of loading or parsing errors @@ -331,7 +330,7 @@ public int registerBeanDefinitions(Map map) throws BeansException { * @param map a map of {@code name} to {@code property} (String or Object). Property * values will be strings if coming from a Properties file etc. Property names * (keys) must be Strings. Class keys must be Strings. - * @param prefix a filter within the keys in the map: e.g. 'beans.' + * @param prefix a filter within the keys in the map: for example, 'beans.' * (can be empty or {@code null}) * @return the number of bean definitions found * @throws BeansException in case of loading or parsing errors @@ -346,7 +345,7 @@ public int registerBeanDefinitions(Map map, @Nullable String prefix) throw * @param map a map of {@code name} to {@code property} (String or Object). Property * values will be strings if coming from a Properties file etc. Property names * (keys) must be Strings. Class keys must be Strings. - * @param prefix a filter within the keys in the map: e.g. 'beans.' + * @param prefix a filter within the keys in the map: for example, 'beans.' * (can be empty or {@code null}) * @param resourceDescription description of the resource that the * Map came from (for logging purposes) 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 8b80359352d3..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 @@ -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. @@ -17,10 +17,14 @@ package org.springframework.beans.factory.support; import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Set; 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; @@ -29,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; @@ -41,6 +44,8 @@ * In the case of inner-beans, the bean name may have been generated. * * @author Phillip Webb + * @author Stephane Nicoll + * @author Juergen Hoeller * @since 6.0 */ public final class RegisteredBean { @@ -53,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, @@ -198,20 +202,39 @@ 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; } /** * Resolve the constructor or factory method to use for this bean. * @return the {@link java.lang.reflect.Constructor} or {@link java.lang.reflect.Method} + * @deprecated in favor of {@link #resolveInstantiationDescriptor()} */ + @Deprecated(since = "6.1.7") public Executable resolveConstructorOrFactoryMethod() { return new ConstructorResolver((AbstractAutowireCapableBeanFactory) getBeanFactory()) .resolveConstructorOrFactoryMethod(getBeanName(), getMergedBeanDefinition()); } + /** + * Resolve the {@linkplain InstantiationDescriptor descriptor} to use to + * instantiate this bean. It defines the {@link java.lang.reflect.Constructor} + * or {@link java.lang.reflect.Method} to use as well as additional metadata. + * @since 6.1.7 + */ + public InstantiationDescriptor resolveInstantiationDescriptor() { + Executable executable = resolveConstructorOrFactoryMethod(); + if (executable instanceof Method method && !Modifier.isStatic(method.getModifiers())) { + String factoryBeanName = getMergedBeanDefinition().getFactoryBeanName(); + if (factoryBeanName != null && this.beanFactory.containsBean(factoryBeanName)) { + return new InstantiationDescriptor(executable, + this.beanFactory.getMergedBeanDefinition(factoryBeanName).getResolvableType().toClass()); + } + } + return new InstantiationDescriptor(executable, executable.getDeclaringClass()); + } + /** * Resolve an autowired argument. * @param descriptor the descriptor for the dependency (field/method/constructor) @@ -221,8 +244,7 @@ public Executable resolveConstructorOrFactoryMethod() { * @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()) @@ -238,6 +260,24 @@ public String toString() { } + /** + * Descriptor for how a bean should be instantiated. While the {@code targetClass} + * is usually the declaring class of the {@code executable} (in case of a constructor + * or a locally declared factory method), there are cases where retaining the actual + * concrete class is necessary (for example, for an inherited factory method). + * @since 6.1.7 + * @param executable the {@link Executable} ({@link java.lang.reflect.Constructor} + * or {@link java.lang.reflect.Method}) to invoke + * @param targetClass the target {@link Class} of the executable + */ + public record InstantiationDescriptor(Executable executable, Class targetClass) { + + public InstantiationDescriptor(Executable executable) { + this(executable, executable.getDeclaringClass()); + } + } + + /** * Resolver used to obtain inner-bean details. */ @@ -245,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 497c60b08098..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 @@ -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,10 +19,11 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Extension of {@link MethodOverride} that represents an arbitrary @@ -97,22 +98,19 @@ public boolean matches(Method method) { @Override public boolean equals(@Nullable Object other) { - return (other instanceof ReplaceOverride that && super.equals(other) && - ObjectUtils.nullSafeEquals(this.methodReplacerBeanName, that.methodReplacerBeanName) && - ObjectUtils.nullSafeEquals(this.typeIdentifiers, that.typeIdentifiers)); + return (other instanceof ReplaceOverride that && super.equals(that) && + this.methodReplacerBeanName.equals(that.methodReplacerBeanName) && + this.typeIdentifiers.equals(that.typeIdentifiers)); } @Override public int hashCode() { - int hashCode = super.hashCode(); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.methodReplacerBeanName); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.typeIdentifiers); - return hashCode; + return Objects.hash(this.methodReplacerBeanName, this.typeIdentifiers); } @Override public String toString() { - return "Replace override for method '" + getMethodName() + "'"; + return "ReplaceOverride for method '" + getMethodName() + "'"; } } 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 c3ac193a77f2..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 @@ -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,29 +26,30 @@ 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; /** * A root bean definition represents the merged bean definition at runtime * that backs a specific bean in a Spring BeanFactory. It might have been created - * from multiple original bean definitions that inherit from each other, e.g. + * from multiple original bean definitions that inherit from each other, for example, * {@link GenericBeanDefinition GenericBeanDefinitions} from XML declarations. * A root bean definition is essentially the 'unified' bean definition view at runtime. * *

    Root bean definitions may also be used for registering individual bean * definitions in the configuration phase. This is particularly applicable for - * programmatic definitions derived from factory methods (e.g. {@code @Bean} methods) - * and instance suppliers (e.g. lambda expressions) which come with extra type metadata + * programmatic definitions derived from factory methods (for example, {@code @Bean} methods) + * and instance suppliers (for example, lambda expressions) which come with extra type metadata * (see {@link #setTargetType(ResolvableType)}/{@link #setResolvedFactoryMethod(Method)}). * *

    Note: The preferred choice for bean definitions derived from declarative sources - * (e.g. XML definitions) is the flexible {@link GenericBeanDefinition} variant. + * (for example, XML definitions) is the flexible {@link GenericBeanDefinition} variant. * GenericBeanDefinition comes with the advantage that it allows for dynamically * defining parent dependencies, not 'hard-coding' the role as a root bean definition, * even supporting parent relationship changes in the bean post-processor phase. @@ -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,7 +263,7 @@ public RootBeanDefinition(RootBeanDefinition original) { @Override - public String getParentName() { + public @Nullable String getParentName() { return null; } @@ -298,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; } @@ -319,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; } @@ -345,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; } @@ -374,7 +357,7 @@ public ResolvableType getResolvableType() { if (returnType != null) { return returnType; } - Method factoryMethod = this.factoryMethodToIntrospect; + Method factoryMethod = getResolvedFactoryMethod(); if (factoryMethod != null) { return ResolvableType.forMethodReturnType(factoryMethod); } @@ -392,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; @@ -401,8 +383,8 @@ public Constructor[] getPreferredConstructors() { if (attribute instanceof Constructor constructor) { return new Constructor[] {constructor}; } - if (attribute instanceof Constructor[]) { - return (Constructor[]) attribute; + if (attribute instanceof Constructor[] constructors) { + return constructors; } throw new IllegalArgumentException("Invalid value type for attribute '" + PREFERRED_CONSTRUCTORS_ATTRIBUTE + "': " + attribute.getClass().getName()); @@ -450,19 +432,13 @@ 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() { - return this.factoryMethodToIntrospect; - } - - @Override - public void setInstanceSupplier(@Nullable Supplier supplier) { - super.setInstanceSupplier(supplier); - Method factoryMethod = (supplier instanceof InstanceSupplier instanceSupplier ? - instanceSupplier.getFactoryMethod() : null); - if (factoryMethod != null) { - setResolvedFactoryMethod(factoryMethod); + public @Nullable Method getResolvedFactoryMethod() { + Method factoryMethod = this.factoryMethodToIntrospect; + if (factoryMethod == null && + getInstanceSupplier() instanceof InstanceSupplier instanceSupplier) { + factoryMethod = instanceSupplier.getFactoryMethod(); } + return factoryMethod; } /** @@ -512,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) @@ -638,7 +614,7 @@ boolean hasAnyExternallyManagedDestroyMethod(String destroyMethod) { } } - private static boolean hasAnyExternallyManagedMethod(Set candidates, String methodName) { + private static boolean hasAnyExternallyManagedMethod(@Nullable Set candidates, String methodName) { if (candidates != null) { for (String candidate : candidates) { int indexOfDot = candidate.lastIndexOf('.'); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ScopeNotActiveException.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ScopeNotActiveException.java index bb7cddaf2a21..32049b11d3d7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ScopeNotActiveException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ScopeNotActiveException.java @@ -20,7 +20,7 @@ /** * A subclass of {@link BeanCreationException} which indicates that the target scope - * is not active, e.g. in case of request or session scope. + * is not active, for example, in case of request or session scope. * * @author Juergen Hoeller * @since 5.3 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 2afdf73924a4..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 @@ -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,9 +16,13 @@ package org.springframework.beans.factory.support; -import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.beans.factory.config.DependencyDescriptor; -import org.springframework.lang.Nullable; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; /** * {@link AutowireCandidateResolver} implementation to use when no annotation @@ -36,47 +40,74 @@ public class SimpleAutowireCandidateResolver implements AutowireCandidateResolve */ public static final SimpleAutowireCandidateResolver INSTANCE = new SimpleAutowireCandidateResolver(); - - @Override - public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) { - return bdHolder.getBeanDefinition().isAutowireCandidate(); - } - - @Override - public boolean isRequired(DependencyDescriptor descriptor) { - return descriptor.isRequired(); - } - - @Override - public boolean hasQualifier(DependencyDescriptor descriptor) { - return false; - } - + /** + * This implementation returns {@code this} as-is. + * @see #INSTANCE + */ @Override - @Nullable - public Object getSuggestedValue(DependencyDescriptor descriptor) { - return null; + public AutowireCandidateResolver cloneIfNecessary() { + return this; } - @Override - @Nullable - public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { - return null; - } - @Override - @Nullable - public Class getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) { - return null; + /** + * 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 + * @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.3 + * @see BeanFactoryUtils#beansOfTypeIncludingAncestors(ListableBeanFactory, Class) + * @see org.springframework.beans.factory.config.BeanDefinition#isAutowireCandidate() + * @see AbstractBeanDefinition#isDefaultCandidate() + */ + public static Map resolveAutowireCandidates(ConfigurableListableBeanFactory lbf, Class type) { + return resolveAutowireCandidates(lbf, type, true, true); } /** - * This implementation returns {@code this} as-is. - * @see #INSTANCE + * 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() */ - @Override - public AutowireCandidateResolver cloneIfNecessary() { - return this; + @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, + includeNonSingletons, allowEagerInit)) { + if (AutowireUtils.isAutowireCandidate(lbf, beanName)) { + Object beanInstance = lbf.getBean(beanName); + if (!(beanInstance instanceof NullBean)) { + candidates.put(beanName, (T) beanInstance); + } + } + } + return candidates; } } 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 b3c509f36d63..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 @@ -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,12 +19,14 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; 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; @@ -49,18 +51,33 @@ 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 reset. - * @param method the factory method currently being invoked or {@code null} - * @since 6.0 + * Invoke the given {@code instanceSupplier} with the factory method exposed + * as being invoked. + * @param method the factory method to expose + * @param instanceSupplier the instance supplier + * @param the type of the instance + * @return the result of the instance supplier + * @since 6.2 */ - public static void setCurrentlyInvokedFactoryMethod(@Nullable Method method) { - currentlyInvokedFactoryMethod.set(method); + public static T instantiateWithFactoryMethod(Method method, Supplier instanceSupplier) { + Method priorInvokedFactoryMethod = currentlyInvokedFactoryMethod.get(); + try { + currentlyInvokedFactoryMethod.set(method); + return instanceSupplier.get(); + } + finally { + if (priorInvokedFactoryMethod != null) { + currentlyInvokedFactoryMethod.set(priorInvokedFactoryMethod); + } + else { + currentlyInvokedFactoryMethod.remove(); + } + } } @@ -72,7 +89,7 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean synchronized (bd.constructorArgumentLock) { constructorToUse = (Constructor) bd.resolvedConstructorOrFactoryMethod; if (constructorToUse == null) { - final Class clazz = bd.getBeanClass(); + Class clazz = bd.getBeanClass(); if (clazz.isInterface()) { throw new BeanInstantiationException(clazz, "Specified class is an interface"); } @@ -105,7 +122,7 @@ protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - final Constructor ctor, Object... args) { + Constructor ctor, Object... args) { if (!bd.hasMethodOverrides()) { return BeanUtils.instantiateClass(ctor, args); @@ -129,54 +146,42 @@ protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - @Nullable Object factoryBean, final Method factoryMethod, Object... args) { - - try { - ReflectionUtils.makeAccessible(factoryMethod); + @Nullable Object factoryBean, Method factoryMethod, @Nullable Object... args) { - Method priorInvokedFactoryMethod = currentlyInvokedFactoryMethod.get(); + return instantiateWithFactoryMethod(factoryMethod, () -> { try { - currentlyInvokedFactoryMethod.set(factoryMethod); + ReflectionUtils.makeAccessible(factoryMethod); Object result = factoryMethod.invoke(factoryBean, args); if (result == null) { result = new NullBean(); } return result; } - finally { - if (priorInvokedFactoryMethod != null) { - currentlyInvokedFactoryMethod.set(priorInvokedFactoryMethod); - } - else { - currentlyInvokedFactoryMethod.remove(); + catch (IllegalArgumentException ex) { + if (factoryBean != null && !factoryMethod.getDeclaringClass().isAssignableFrom(factoryBean.getClass())) { + throw new BeanInstantiationException(factoryMethod, + "Illegal factory instance for factory method '" + factoryMethod.getName() + "'; " + + "instance: " + factoryBean.getClass().getName(), ex); } + throw new BeanInstantiationException(factoryMethod, + "Illegal arguments to factory method '" + factoryMethod.getName() + "'; " + + "args: " + StringUtils.arrayToCommaDelimitedString(args), ex); } - } - catch (IllegalArgumentException ex) { - if (factoryBean != null - && !factoryMethod.getDeclaringClass().isAssignableFrom(factoryBean.getClass())) { + catch (IllegalAccessException ex) { throw new BeanInstantiationException(factoryMethod, - "Illegal factory instance for factory method '" + factoryMethod.getName() + "'; " + - "instance: " + factoryBean.getClass().getName(), ex); + "Cannot access factory method '" + factoryMethod.getName() + "'; is it public?", ex); } - throw new BeanInstantiationException(factoryMethod, - "Illegal arguments to factory method '" + factoryMethod.getName() + "'; " + - "args: " + StringUtils.arrayToCommaDelimitedString(args), ex); - } - catch (IllegalAccessException ex) { - throw new BeanInstantiationException(factoryMethod, - "Cannot access factory method '" + factoryMethod.getName() + "'; is it public?", ex); - } - catch (InvocationTargetException ex) { - String msg = "Factory method '" + factoryMethod.getName() + "' threw exception with message: " + - ex.getTargetException().getMessage(); - if (bd.getFactoryBeanName() != null && owner instanceof ConfigurableBeanFactory cbf && - cbf.isCurrentlyInCreation(bd.getFactoryBeanName())) { - msg = "Circular reference involving containing bean '" + bd.getFactoryBeanName() + "' - consider " + - "declaring the factory method as static for independence from its containing instance. " + msg; + catch (InvocationTargetException ex) { + String msg = "Factory method '" + factoryMethod.getName() + "' threw exception with message: " + + ex.getTargetException().getMessage(); + if (bd.getFactoryBeanName() != null && owner instanceof ConfigurableBeanFactory cbf && + cbf.isCurrentlyInCreation(bd.getFactoryBeanName())) { + msg = "Circular reference involving containing bean '" + bd.getFactoryBeanName() + "' - consider " + + "declaring the factory method as static for independence from its containing instance. " + msg; + } + throw new BeanInstantiationException(factoryMethod, msg, ex.getTargetException()); } - throw new BeanInstantiationException(factoryMethod, msg, ex.getTargetException()); - } + }); } } 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 4fded794a043..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 @@ -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.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; @@ -37,10 +39,8 @@ import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.SmartFactoryBean; -import org.springframework.core.OrderComparator; 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; @@ -155,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"); @@ -178,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"); @@ -232,12 +232,12 @@ public boolean isTypeMatch(String name, @Nullable Class typeToMatch) throws N } @Override - public Class getType(String name) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name) throws NoSuchBeanDefinitionException { return getType(name, true); } @Override - 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); @@ -301,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); @@ -314,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]); @@ -328,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]); @@ -342,10 +340,6 @@ public T getIfUnique() throws BeansException { public Stream stream() { return Arrays.stream(getBeanNamesForType(requiredType)).map(name -> (T) getBean(name)); } - @Override - public Stream orderedStream() { - return stream().sorted(OrderComparator.INSTANCE); - } }; } @@ -455,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 a44eb84416ec..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,8 +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 - 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); @@ -460,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; } } @@ -496,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)); @@ -889,7 +885,7 @@ public void parseQualifierElement(Element ele, AbstractBeanDefinition bd) { qualifier.addMetadataAttribute(attribute); } else { - error("Qualifier 'attribute' tag must have a 'name' and 'value'", attributeEle); + error("Qualifier 'attribute' tag must have a 'key' and 'value'", attributeEle); return; } } @@ -905,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"); @@ -966,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); } @@ -979,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); } @@ -1049,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)) { @@ -1303,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++) { @@ -1365,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); } @@ -1376,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; @@ -1464,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. " + @@ -1489,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 b8e2935a9c46..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 @@ -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,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); } @@ -126,9 +123,10 @@ protected void doRegisterBeanDefinitions(Element root) { // then ultimately reset this.delegate back to its original (parent) reference. // this behavior emulates a stack of delegates without actually necessitating one. BeanDefinitionParserDelegate parent = this.delegate; - this.delegate = createDelegate(getReaderContext(), root, parent); + BeanDefinitionParserDelegate current = createDelegate(getReaderContext(), root, parent); + this.delegate = current; - if (this.delegate.isDefaultNamespace(root)) { + if (current.isDefaultNamespace(root)) { String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); if (StringUtils.hasText(profileSpec)) { String[] specifiedProfiles = StringUtils.tokenizeToStringArray( @@ -146,7 +144,7 @@ protected void doRegisterBeanDefinitions(Element root) { } preProcessXml(root); - parseBeanDefinitions(root, this.delegate); + parseBeanDefinitions(root, current); postProcessXml(root); this.delegate = parent; @@ -212,7 +210,7 @@ protected void importBeanDefinitionResource(Element ele) { return; } - // Resolve system properties: e.g. "${user.dir}" + // Resolve system properties: for example, "${user.dir}" location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location); Set actualResources = new LinkedHashSet<>(4); 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 08b1d16f2778..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,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; /** @@ -88,6 +88,9 @@ public Document loadDocument(InputSource inputSource, EntityResolver entityResol protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware) throws ParserConfigurationException { + // This document loader is used for loading application configuration files. + // As a result, attackers would need complete write access to application configuration + // to leverage XXE attacks. This does not qualify as privilege escalation. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(namespaceAware); 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 ec7c7d4c9b2c..14583451913f 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 @@ -18,7 +18,8 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -30,7 +31,7 @@ * {@link org.springframework.beans.BeanWrapperImpl} will register this * editor by default. * - *

    Also supports conversion from a Unicode character sequence; e.g. + *

    Also supports conversion from a Unicode character sequence; for example, * {@code u0041} ('A'). * * @author Juergen Hoeller diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java index ef772db749cb..5fb86ae962ad 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java @@ -26,7 +26,7 @@ * String representations into Charset objects and back. * *

    Expects the same syntax as Charset's {@link java.nio.charset.Charset#name()}, - * e.g. {@code UTF-8}, {@code ISO-8859-16}, etc. + * for example, {@code UTF-8}, {@code ISO-8859-16}, etc. * * @author Arjen Poutsma * @author Sam Brannen 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 fca24a5418ef..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,15 +19,16 @@ 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; /** * One-way PropertyEditor which can convert from a text String to a * {@code java.io.InputStream}, interpreting the given String as a - * Spring resource location (e.g. a URL String). + * Spring resource location (for example, a URL String). * *

    Supports Spring-style URL notation: any fully qualified standard URL * ("file:", "http:", etc.) and Spring's special "classpath:" pseudo-URL. @@ -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/LocaleEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java index 5a327f710e0c..85eedaac60a2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.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. @@ -24,19 +24,19 @@ * Editor for {@code java.util.Locale}, to directly populate a Locale property. * *

    Expects the same syntax as Locale's {@code toString()}, i.e. language + - * optionally country + optionally variant, separated by "_" (e.g. "en", "en_US"). + * optionally country + optionally variant, separated by "_" (for example, "en", "en_US"). * Also accepts spaces as separators, as an alternative to underscores. * * @author Juergen Hoeller * @since 26.05.2003 * @see java.util.Locale - * @see org.springframework.util.StringUtils#parseLocaleString + * @see org.springframework.util.StringUtils#parseLocale */ public class LocaleEditor extends PropertyEditorSupport { @Override public void setAsText(String text) { - setValue(StringUtils.parseLocaleString(text)); + setValue(StringUtils.parseLocale(text)); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java index 13226e6ca0db..70e348f09d2b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.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. @@ -78,8 +78,10 @@ public void setAsText(String text) throws IllegalArgumentException { if (nioPathCandidate && !text.startsWith("/")) { try { URI uri = ResourceUtils.toURI(text); - if (uri.getScheme() != null) { - nioPathCandidate = false; + String scheme = uri.getScheme(); + if (scheme != null) { + // No NIO candidate except for "C:" style drive letters + nioPathCandidate = (scheme.length() == 1); // Let's try NIO file system providers via Paths.get(URI) setValue(Paths.get(uri).normalize()); return; @@ -90,9 +92,9 @@ public void setAsText(String text) throws IllegalArgumentException { // a file prefix (let's try as Spring resource location) nioPathCandidate = !text.startsWith(ResourceUtils.FILE_URL_PREFIX); } - catch (FileSystemNotFoundException ex) { - // URI scheme not registered for NIO (let's try URL - // protocol handlers via Spring's resource mechanism). + catch (FileSystemNotFoundException | IllegalArgumentException ex) { + // URI scheme not registered for NIO or not meeting Paths requirements: + // let's try URL protocol handlers via Spring's resource mechanism. } } @@ -109,7 +111,13 @@ else if (nioPathCandidate && !resource.exists()) { setValue(resource.getFile().toPath()); } catch (IOException ex) { - throw new IllegalArgumentException("Failed to retrieve file for " + resource, ex); + String msg = "Could not resolve \"" + text + "\" to 'java.nio.file.Path' for " + resource + ": " + + ex.getMessage(); + if (nioPathCandidate) { + msg += " - In case of ambiguity, consider adding the 'file:' prefix for an explicit reference " + + "to a file system resource of the same name: \"file:" + text + "\""; + } + throw new IllegalArgumentException(msg); } } } 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 a388932bfca0..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,16 +19,17 @@ 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; /** * One-way PropertyEditor which can convert from a text String to a * {@code java.io.Reader}, interpreting the given String as a Spring - * resource location (e.g. a URL String). + * resource location (for example, a URL String). * *

    Supports Spring-style URL notation: any fully qualified standard URL * ("file:", "http:", etc.) and Spring's special "classpath:" pseudo-URL. @@ -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 e278c8721b65..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; @@ -97,7 +97,7 @@ public StringArrayPropertyEditor(String separator, boolean emptyArrayAsNull, boo * @param separator the separator to use for splitting a {@link String} * @param charsToDelete a set of characters to delete, in addition to * trimming an input String. Useful for deleting unwanted line breaks: - * e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * for example, "\r\n\f" will delete all new lines and line feeds in a String. * @param emptyArrayAsNull {@code true} if an empty String array * is to be transformed into {@code null} */ @@ -110,7 +110,7 @@ public StringArrayPropertyEditor(String separator, @Nullable String charsToDelet * @param separator the separator to use for splitting a {@link String} * @param charsToDelete a set of characters to delete, in addition to * trimming an input String. Useful for deleting unwanted line breaks: - * e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * for example, "\r\n\f" will delete all new lines and line feeds in a String. * @param emptyArrayAsNull {@code true} if an empty String array * is to be transformed into {@code null} * @param trimValues {@code true} if the values in the parsed arrays @@ -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 0fbbfd327ba7..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,22 +18,22 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** * Property editor that trims Strings. * *

    Optionally allows transforming an empty string into a {@code null} value. - * Needs to be explicitly registered, e.g. for command binding. + * Needs to be explicitly registered, for example, for command binding. * * @author Juergen Hoeller * @see org.springframework.validation.DataBinder#registerCustomEditor */ public class StringTrimmerEditor extends PropertyEditorSupport { - @Nullable - private final String charsToDelete; + private final @Nullable String charsToDelete; private final boolean emptyAsNull; @@ -52,7 +52,7 @@ public StringTrimmerEditor(boolean emptyAsNull) { * Create a new StringTrimmerEditor. * @param charsToDelete a set of characters to delete, in addition to * trimming an input String. Useful for deleting unwanted line breaks: - * e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * for example, "\r\n\f" will delete all new lines and line feeds in a String. * @param emptyAsNull {@code true} if an empty String is to be * transformed into {@code null} */ 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/ZoneIdEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java index 4992e33aebd0..cee4801e66ba 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.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. @@ -17,16 +17,18 @@ package org.springframework.beans.propertyeditors; import java.beans.PropertyEditorSupport; +import java.time.DateTimeException; import java.time.ZoneId; import org.springframework.util.StringUtils; /** - * Editor for {@code java.time.ZoneId}, translating zone ID Strings into {@code ZoneId} - * objects. Exposes the {@code TimeZone} ID as a text representation. + * Editor for {@code java.time.ZoneId}, translating time zone Strings into {@code ZoneId} + * objects. Exposes the time zone as a text representation. * * @author Nicholas Williams * @author Sam Brannen + * @author Juergen Hoeller * @since 4.0 * @see java.time.ZoneId * @see TimeZoneEditor @@ -38,7 +40,12 @@ public void setAsText(String text) throws IllegalArgumentException { if (StringUtils.hasText(text)) { text = text.trim(); } - setValue(ZoneId.of(text)); + try { + setValue(ZoneId.of(text)); + } + catch (DateTimeException ex) { + throw new IllegalArgumentException(ex.getMessage(), ex); + } } @Override 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 0c33bd276e0c..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 @@ -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,13 +19,14 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; +import java.util.Locale; 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; /** @@ -77,8 +78,8 @@ public int compare(T o1, T o2) { Object v1 = getPropertyValue(o1); Object v2 = getPropertyValue(o2); if (this.sortDefinition.isIgnoreCase() && (v1 instanceof String text1) && (v2 instanceof String text2)) { - v1 = text1.toLowerCase(); - v2 = text2.toLowerCase(); + v1 = text1.toLowerCase(Locale.ROOT); + v2 = text2.toLowerCase(Locale.ROOT); } int result; @@ -107,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/ResourceEditorRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java index f5c931217696..45ea32de3fcf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.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. @@ -135,4 +135,12 @@ private void doRegisterEditor(PropertyEditorRegistry registry, Class required } } + /** + * Indicate the use of {@link PropertyEditorRegistrySupport#overrideDefaultEditor} above. + */ + @Override + public boolean overridesDefaultEditors() { + return true; + } + } 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..8daf689cf4be --- /dev/null +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt @@ -0,0 +1,421 @@ +/* + * Copyright 2002-2025 the original author 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 from the given bean class, 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 from the given bean class, 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 from the given bean class, 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).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 from the given bean class, 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).supplier() + } + val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); + if (resolvableType.hasGenerics()) { + it.targetType(resolvableType) + } + } + return registry.registerBean(T::class.java, customizer) + } + + + /** + * Context available from the bean instance supplier designed to give access + * to bean dependencies. + */ + @BeanRegistrarDslMarker + open class SupplierContextDsl(@PublishedApi internal val context: SupplierContext) { + + /** + * 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/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd index 42f487cfeb3f..27b7b3434714 100644 --- a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd +++ b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd @@ -137,8 +137,8 @@ @@ -343,14 +343,14 @@ @@ -451,8 +451,8 @@ "); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java index faa41995e6d5..0aa52baa1623 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -31,10 +31,10 @@ * @author Rob Harrop * @author Rick Evans */ -public class DefaultNamespaceHandlerResolverTests { +class DefaultNamespaceHandlerResolverTests { @Test - public void testResolvedMappedHandler() { + void testResolvedMappedHandler() { DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver(getClass().getClassLoader()); NamespaceHandler handler = resolver.resolve("http://www.springframework.org/schema/util"); assertThat(handler).as("Handler should not be null.").isNotNull(); @@ -42,7 +42,7 @@ public void testResolvedMappedHandler() { } @Test - public void testResolvedMappedHandlerWithNoArgCtor() { + void testResolvedMappedHandlerWithNoArgCtor() { DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver(); NamespaceHandler handler = resolver.resolve("http://www.springframework.org/schema/util"); assertThat(handler).as("Handler should not be null.").isNotNull(); @@ -50,25 +50,25 @@ public void testResolvedMappedHandlerWithNoArgCtor() { } @Test - public void testNonExistentHandlerClass() { + void testNonExistentHandlerClass() { String mappingPath = "org/springframework/beans/factory/xml/support/nonExistent.properties"; new DefaultNamespaceHandlerResolver(getClass().getClassLoader(), mappingPath); } @Test - public void testCtorWithNullClassLoaderArgument() { + void testCtorWithNullClassLoaderArgument() { // simply must not bail... new DefaultNamespaceHandlerResolver(null); } @Test - public void testCtorWithNullClassLoaderArgumentAndNullMappingLocationArgument() { + void testCtorWithNullClassLoaderArgumentAndNullMappingLocationArgument() { assertThatIllegalArgumentException().isThrownBy(() -> new DefaultNamespaceHandlerResolver(null, null)); } @Test - public void testCtorWithNonExistentMappingLocationArgument() { + void testCtorWithNonExistentMappingLocationArgument() { // simply must not bail; we don't want non-existent resources to result in an Exception new DefaultNamespaceHandlerResolver(null, "738trbc bobabloobop871"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java index 7b91fc60e64a..73dbf7124585 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.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. @@ -33,10 +33,10 @@ * @author Juergen Hoeller * @since 06.03.2006 */ -public class BeanInfoTests { +class BeanInfoTests { @Test - public void testComplexObject() { + void testComplexObject() { ValueBean bean = new ValueBean(); BeanWrapper bw = new BeanWrapperImpl(bean); Integer value = 1; diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java index ef5bdc87d761..1b74fba99358 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.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,16 +23,16 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for the {@link ByteArrayPropertyEditor} class. + * Tests for {@link ByteArrayPropertyEditor}. * * @author Rick Evans */ -public class ByteArrayPropertyEditorTests { +class ByteArrayPropertyEditorTests { private final PropertyEditor byteEditor = new ByteArrayPropertyEditor(); @Test - public void sunnyDaySetAsText() throws Exception { + void sunnyDaySetAsText() { final String text = "Hideous towns make me throw... up"; byteEditor.setAsText(text); @@ -46,7 +46,7 @@ public void sunnyDaySetAsText() throws Exception { } @Test - public void getAsTextReturnsEmptyStringIfValueIsNull() throws Exception { + void getAsTextReturnsEmptyStringIfValueIsNull() { assertThat(byteEditor.getAsText()).isEmpty(); byteEditor.setAsText(null); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java index af09ae6c6e0d..e6dda7de2884 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.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,16 +23,16 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for the {@link CharArrayPropertyEditor} class. + * Tests for {@link CharArrayPropertyEditor}. * * @author Rick Evans */ -public class CharArrayPropertyEditorTests { +class CharArrayPropertyEditorTests { private final PropertyEditor charEditor = new CharArrayPropertyEditor(); @Test - public void sunnyDaySetAsText() throws Exception { + void sunnyDaySetAsText() { final String text = "Hideous towns make me throw... up"; charEditor.setAsText(text); @@ -46,7 +46,7 @@ public void sunnyDaySetAsText() throws Exception { } @Test - public void getAsTextReturnsEmptyStringIfValueIsNull() throws Exception { + void getAsTextReturnsEmptyStringIfValueIsNull() { assertThat(charEditor.getAsText()).isEmpty(); charEditor.setAsText(null); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java index d5597923f735..cc6f6f14d7d0 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.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. @@ -24,17 +24,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.InstanceOfAssertFactories.LIST; /** - * Unit tests for the {@link CustomCollectionEditor} class. + * Tests for {@link CustomCollectionEditor}. * * @author Rick Evans * @author Chris Beams */ -public class CustomCollectionEditorTests { +class CustomCollectionEditorTests { @Test - public void testCtorWithNullCollectionType() { + void testCtorWithNullCollectionType() { assertThatIllegalArgumentException().isThrownBy(() -> new CustomCollectionEditor(null)); } @@ -47,46 +48,42 @@ public void testCtorWithNonCollectionType() { } @Test - public void testWithCollectionTypeThatDoesNotExposeAPublicNoArgCtor() { + void testWithCollectionTypeThatDoesNotExposeAPublicNoArgCtor() { CustomCollectionEditor editor = new CustomCollectionEditor(CollectionTypeWithNoNoArgCtor.class); assertThatIllegalArgumentException().isThrownBy(() -> editor.setValue("1")); } @Test - public void testSunnyDaySetValue() { + void testSunnyDaySetValue() { CustomCollectionEditor editor = new CustomCollectionEditor(ArrayList.class); editor.setValue(new int[] {0, 1, 2}); Object value = editor.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof ArrayList).isTrue(); - List list = (List) value; - assertThat(list).as("There must be 3 elements in the converted collection").hasSize(3); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); + assertThat(value).isInstanceOf(ArrayList.class); + assertThat(value).asInstanceOf(LIST).containsExactly(0, 1, 2); } @Test - public void testWhenTargetTypeIsExactlyTheCollectionInterfaceUsesFallbackCollectionType() { + void testWhenTargetTypeIsExactlyTheCollectionInterfaceUsesFallbackCollectionType() { CustomCollectionEditor editor = new CustomCollectionEditor(Collection.class); editor.setValue("0, 1, 2"); Collection value = (Collection) editor.getValue(); assertThat(value).isNotNull(); assertThat(value).as("There must be 1 element in the converted collection").hasSize(1); - assertThat(value.iterator().next()).isEqualTo("0, 1, 2"); + assertThat(value).singleElement().isEqualTo("0, 1, 2"); } @Test - public void testSunnyDaySetAsTextYieldsSingleValue() { + void testSunnyDaySetAsTextYieldsSingleValue() { CustomCollectionEditor editor = new CustomCollectionEditor(ArrayList.class); editor.setValue("0, 1, 2"); Object value = editor.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof ArrayList).isTrue(); + assertThat(value).isInstanceOf(ArrayList.class); List list = (List) value; assertThat(list).as("There must be 1 element in the converted collection").hasSize(1); - assertThat(list.get(0)).isEqualTo("0, 1, 2"); + assertThat(list).singleElement().isEqualTo("0, 1, 2"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java index b744cd94aa99..e335d1516e77 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.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,7 +18,6 @@ import java.beans.PropertyEditor; import java.beans.PropertyEditorSupport; -import java.beans.PropertyVetoException; import java.io.File; import java.math.BigDecimal; import java.math.BigInteger; @@ -55,7 +54,7 @@ import static org.assertj.core.api.Assertions.within; /** - * Unit tests for the various PropertyEditors in Spring. + * Tests for the various PropertyEditors in Spring. * * @author Juergen Hoeller * @author Rick Evans @@ -67,7 +66,7 @@ class CustomEditorTests { @Test - void testComplexObject() { + void complexObject() { TestBean tb = new TestBean(); String newName = "Rod"; String tbString = "Kerry_34"; @@ -85,7 +84,7 @@ void testComplexObject() { } @Test - void testComplexObjectWithOldValueAccess() { + void complexObjectWithOldValueAccess() { TestBean tb = new TestBean(); String newName = "Rod"; String tbString = "Kerry_34"; @@ -109,7 +108,7 @@ void testComplexObjectWithOldValueAccess() { } @Test - void testCustomEditorForSingleProperty() { + void customEditorForSingleProperty() { TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { @@ -127,7 +126,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testCustomEditorForAllStringProperties() { + void customEditorForAllStringProperties() { TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(String.class, new PropertyEditorSupport() { @@ -145,7 +144,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testCustomEditorForSingleNestedProperty() { + void customEditorForSingleNestedProperty() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -164,7 +163,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testCustomEditorForAllNestedStringProperties() { + void customEditorForAllNestedStringProperties() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -183,7 +182,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testDefaultBooleanEditorForPrimitiveType() { + void defaultBooleanEditorForPrimitiveType() { BooleanTestBean tb = new BooleanTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -193,38 +192,38 @@ void testDefaultBooleanEditorForPrimitiveType() { bw.setPropertyValue("bool1", "false"); assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool1"))).as("Correct bool1 value").isTrue(); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); bw.setPropertyValue("bool1", " true "); assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); bw.setPropertyValue("bool1", " false "); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); bw.setPropertyValue("bool1", "on"); assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); bw.setPropertyValue("bool1", "off"); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); bw.setPropertyValue("bool1", "yes"); assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); bw.setPropertyValue("bool1", "no"); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); bw.setPropertyValue("bool1", "1"); assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); bw.setPropertyValue("bool1", "0"); - assertThat(!tb.isBool1()).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isFalse(); assertThatExceptionOfType(BeansException.class).isThrownBy(() -> bw.setPropertyValue("bool1", "argh")); } @Test - void testDefaultBooleanEditorForWrapperType() { + void defaultBooleanEditorForWrapperType() { BooleanTestBean tb = new BooleanTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -234,32 +233,32 @@ void testDefaultBooleanEditorForWrapperType() { bw.setPropertyValue("bool2", "false"); assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "on"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "off"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "yes"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "no"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "1"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "0"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", ""); assertThat(tb.getBool2()).as("Correct bool2 value").isNull(); } @Test - void testCustomBooleanEditorWithAllowEmpty() { + void customBooleanEditorWithAllowEmpty() { BooleanTestBean tb = new BooleanTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(Boolean.class, new CustomBooleanEditor(true)); @@ -270,25 +269,25 @@ void testCustomBooleanEditorWithAllowEmpty() { bw.setPropertyValue("bool2", "false"); assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "on"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "off"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "yes"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "no"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", "1"); assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); bw.setPropertyValue("bool2", "0"); - assertThat(!tb.getBool2()).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2()).as("Correct bool2 value").isFalse(); bw.setPropertyValue("bool2", ""); assertThat(bw.getPropertyValue("bool2")).as("Correct bool2 value").isNull(); @@ -296,7 +295,7 @@ void testCustomBooleanEditorWithAllowEmpty() { } @Test - void testCustomBooleanEditorWithSpecialTrueAndFalseStrings() { + void customBooleanEditorWithSpecialTrueAndFalseStrings() { String trueString = "pechorin"; String falseString = "nash"; @@ -320,7 +319,7 @@ void testCustomBooleanEditorWithSpecialTrueAndFalseStrings() { } @Test - void testDefaultNumberEditor() { + void defaultNumberEditor() { NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -364,7 +363,7 @@ void testDefaultNumberEditor() { } @Test - void testCustomNumberEditorWithoutAllowEmpty() { + void customNumberEditorWithoutAllowEmpty() { NumberFormat nf = NumberFormat.getNumberInstance(Locale.GERMAN); NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -421,7 +420,7 @@ void testCustomNumberEditorWithoutAllowEmpty() { } @Test - void testCustomNumberEditorWithAllowEmpty() { + void customNumberEditorWithAllowEmpty() { NumberFormat nf = NumberFormat.getNumberInstance(Locale.GERMAN); NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -445,7 +444,7 @@ void testCustomNumberEditorWithAllowEmpty() { } @Test - void testCustomNumberEditorWithFrenchBigDecimal() { + void customNumberEditorWithFrenchBigDecimal() { NumberFormat nf = NumberFormat.getNumberInstance(Locale.FRENCH); NumberTestBean tb = new NumberTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); @@ -462,14 +461,14 @@ void testCustomNumberEditorWithFrenchBigDecimal() { } @Test - void testParseShortGreaterThanMaxValueWithoutNumberFormat() { + void parseShortGreaterThanMaxValueWithoutNumberFormat() { CustomNumberEditor editor = new CustomNumberEditor(Short.class, true); assertThatExceptionOfType(NumberFormatException.class).as("greater than Short.MAX_VALUE + 1").isThrownBy(() -> editor.setAsText(String.valueOf(Short.MAX_VALUE + 1))); } @Test - void testByteArrayPropertyEditor() { + void byteArrayPropertyEditor() { PrimitiveArrayBean bean = new PrimitiveArrayBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.setPropertyValue("byteArray", "myvalue"); @@ -477,7 +476,7 @@ void testByteArrayPropertyEditor() { } @Test - void testCharArrayPropertyEditor() { + void charArrayPropertyEditor() { PrimitiveArrayBean bean = new PrimitiveArrayBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.setPropertyValue("charArray", "myvalue"); @@ -485,7 +484,7 @@ void testCharArrayPropertyEditor() { } @Test - void testCharacterEditor() { + void characterEditor() { CharBean cb = new CharBean(); BeanWrapper bw = new BeanWrapperImpl(cb); @@ -507,7 +506,7 @@ void testCharacterEditor() { } @Test - void testCharacterEditorWithAllowEmpty() { + void characterEditorWithAllowEmpty() { CharBean cb = new CharBean(); BeanWrapper bw = new BeanWrapperImpl(cb); bw.registerCustomEditor(Character.class, new CharacterEditor(true)); @@ -529,14 +528,14 @@ void testCharacterEditorWithAllowEmpty() { } @Test - void testCharacterEditorSetAsTextWithStringLongerThanOneCharacter() { + void characterEditorSetAsTextWithStringLongerThanOneCharacter() { PropertyEditor charEditor = new CharacterEditor(false); assertThatIllegalArgumentException().isThrownBy(() -> charEditor.setAsText("ColdWaterCanyon")); } @Test - void testCharacterEditorGetAsTextReturnsEmptyStringIfValueIsNull() { + void characterEditorGetAsTextReturnsEmptyStringIfValueIsNull() { PropertyEditor charEditor = new CharacterEditor(false); assertThat(charEditor.getAsText()).isEmpty(); charEditor = new CharacterEditor(true); @@ -549,14 +548,14 @@ void testCharacterEditorGetAsTextReturnsEmptyStringIfValueIsNull() { } @Test - void testCharacterEditorSetAsTextWithNullNotAllowingEmptyAsNull() { + void characterEditorSetAsTextWithNullNotAllowingEmptyAsNull() { PropertyEditor charEditor = new CharacterEditor(false); assertThatIllegalArgumentException().isThrownBy(() -> charEditor.setAsText(null)); } @Test - void testClassEditor() { + void classEditor() { PropertyEditor classEditor = new ClassEditor(); classEditor.setAsText(TestBean.class.getName()); assertThat(classEditor.getValue()).isEqualTo(TestBean.class); @@ -571,14 +570,14 @@ void testClassEditor() { } @Test - void testClassEditorWithNonExistentClass() { + void classEditorWithNonExistentClass() { PropertyEditor classEditor = new ClassEditor(); assertThatIllegalArgumentException().isThrownBy(() -> classEditor.setAsText("hairdresser.on.Fire")); } @Test - void testClassEditorWithArray() { + void classEditorWithArray() { PropertyEditor classEditor = new ClassEditor(); classEditor.setAsText("org.springframework.beans.testfixture.beans.TestBean[]"); assertThat(classEditor.getValue()).isEqualTo(TestBean[].class); @@ -589,7 +588,7 @@ void testClassEditorWithArray() { * SPR_2165 - ClassEditor is inconsistent with multidimensional arrays */ @Test - void testGetAsTextWithTwoDimensionalArray() { + void getAsTextWithTwoDimensionalArray() { String[][] chessboard = new String[8][8]; ClassEditor editor = new ClassEditor(); editor.setValue(chessboard.getClass()); @@ -600,7 +599,7 @@ void testGetAsTextWithTwoDimensionalArray() { * SPR_2165 - ClassEditor is inconsistent with multidimensional arrays */ @Test - void testGetAsTextWithRidiculousMultiDimensionalArray() { + void getAsTextWithRidiculousMultiDimensionalArray() { String[][][][][] ridiculousChessboard = new String[8][4][0][1][3]; ClassEditor editor = new ClassEditor(); editor.setValue(ridiculousChessboard.getClass()); @@ -608,7 +607,7 @@ void testGetAsTextWithRidiculousMultiDimensionalArray() { } @Test - void testFileEditor() { + void fileEditor() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("file:myfile.txt"); assertThat(fileEditor.getValue()).isEqualTo(new File("myfile.txt")); @@ -616,7 +615,7 @@ void testFileEditor() { } @Test - void testFileEditorWithRelativePath() { + void fileEditorWithRelativePath() { PropertyEditor fileEditor = new FileEditor(); try { fileEditor.setAsText("myfile.txt"); @@ -628,7 +627,7 @@ void testFileEditorWithRelativePath() { } @Test - void testFileEditorWithAbsolutePath() { + void fileEditorWithAbsolutePath() { PropertyEditor fileEditor = new FileEditor(); // testing on Windows if (new File("C:/myfile.txt").isAbsolute()) { @@ -643,18 +642,22 @@ void testFileEditorWithAbsolutePath() { } @Test - void testLocaleEditor() { + void localeEditor() { PropertyEditor localeEditor = new LocaleEditor(); localeEditor.setAsText("en_CA"); assertThat(localeEditor.getValue()).isEqualTo(Locale.CANADA); assertThat(localeEditor.getAsText()).isEqualTo("en_CA"); + localeEditor = new LocaleEditor(); + localeEditor.setAsText("zh-Hans"); + assertThat(localeEditor.getValue()).isEqualTo(Locale.forLanguageTag("zh-Hans")); + localeEditor = new LocaleEditor(); assertThat(localeEditor.getAsText()).isEmpty(); } @Test - void testPatternEditor() { + void patternEditor() { final String REGEX = "a.*"; PropertyEditor patternEditor = new PatternEditor(); @@ -671,7 +674,7 @@ void testPatternEditor() { } @Test - void testCustomBooleanEditor() { + void customBooleanEditor() { CustomBooleanEditor editor = new CustomBooleanEditor(false); editor.setAsText("true"); @@ -691,7 +694,7 @@ void testCustomBooleanEditor() { } @Test - void testCustomBooleanEditorWithEmptyAsNull() { + void customBooleanEditorWithEmptyAsNull() { CustomBooleanEditor editor = new CustomBooleanEditor(true); editor.setAsText("true"); @@ -708,7 +711,7 @@ void testCustomBooleanEditorWithEmptyAsNull() { } @Test - void testCustomDateEditor() { + void customDateEditor() { CustomDateEditor editor = new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), false); editor.setValue(null); assertThat(editor.getValue()).isNull(); @@ -716,7 +719,7 @@ void testCustomDateEditor() { } @Test - void testCustomDateEditorWithEmptyAsNull() { + void customDateEditorWithEmptyAsNull() { CustomDateEditor editor = new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), true); editor.setValue(null); assertThat(editor.getValue()).isNull(); @@ -724,7 +727,7 @@ void testCustomDateEditorWithEmptyAsNull() { } @Test - void testCustomDateEditorWithExactDateLength() { + void customDateEditorWithExactDateLength() { int maxLength = 10; String validDate = "01/01/2005"; String invalidDate = "01/01/05"; @@ -740,7 +743,7 @@ void testCustomDateEditorWithExactDateLength() { } @Test - void testCustomNumberEditor() { + void customNumberEditor() { CustomNumberEditor editor = new CustomNumberEditor(Integer.class, false); editor.setAsText("5"); assertThat(editor.getValue()).isEqualTo(5); @@ -751,14 +754,14 @@ void testCustomNumberEditor() { } @Test - void testCustomNumberEditorWithHex() { + void customNumberEditorWithHex() { CustomNumberEditor editor = new CustomNumberEditor(Integer.class, false); editor.setAsText("0x" + Integer.toHexString(64)); assertThat(editor.getValue()).isEqualTo(64); } @Test - void testCustomNumberEditorWithEmptyAsNull() { + void customNumberEditorWithEmptyAsNull() { CustomNumberEditor editor = new CustomNumberEditor(Integer.class, true); editor.setAsText("5"); assertThat(editor.getValue()).isEqualTo(5); @@ -772,7 +775,7 @@ void testCustomNumberEditorWithEmptyAsNull() { } @Test - void testStringTrimmerEditor() { + void stringTrimmerEditor() { StringTrimmerEditor editor = new StringTrimmerEditor(false); editor.setAsText("test"); assertThat(editor.getValue()).isEqualTo("test"); @@ -790,7 +793,7 @@ void testStringTrimmerEditor() { } @Test - void testStringTrimmerEditorWithEmptyAsNull() { + void stringTrimmerEditorWithEmptyAsNull() { StringTrimmerEditor editor = new StringTrimmerEditor(true); editor.setAsText("test"); assertThat(editor.getValue()).isEqualTo("test"); @@ -806,7 +809,7 @@ void testStringTrimmerEditorWithEmptyAsNull() { } @Test - void testStringTrimmerEditorWithCharsToDelete() { + void stringTrimmerEditorWithCharsToDelete() { StringTrimmerEditor editor = new StringTrimmerEditor("\r\n\f", false); editor.setAsText("te\ns\ft"); assertThat(editor.getValue()).isEqualTo("test"); @@ -822,7 +825,7 @@ void testStringTrimmerEditorWithCharsToDelete() { } @Test - void testStringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { + void stringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { StringTrimmerEditor editor = new StringTrimmerEditor("\r\n\f", true); editor.setAsText("te\ns\ft"); assertThat(editor.getValue()).isEqualTo("test"); @@ -838,7 +841,7 @@ void testStringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { } @Test - void testIndexedPropertiesWithCustomEditorForType() { + void indexedPropertiesWithCustomEditorForType() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(String.class, new PropertyEditorSupport() { @@ -891,7 +894,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testIndexedPropertiesWithCustomEditorForProperty() { + void indexedPropertiesWithCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(String.class, "array.name", new PropertyEditorSupport() { @@ -958,7 +961,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testIndexedPropertiesWithIndividualCustomEditorForProperty() { + void indexedPropertiesWithIndividualCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(String.class, "array[0].name", new PropertyEditorSupport() { @@ -1043,7 +1046,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testNestedIndexedPropertiesWithCustomEditorForProperty() { + void nestedIndexedPropertiesWithCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(); TestBean tb0 = bean.getArray()[0]; TestBean tb1 = bean.getArray()[1]; @@ -1127,7 +1130,7 @@ public String getAsText() { } @Test - void testNestedIndexedPropertiesWithIndexedCustomEditorForProperty() { + void nestedIndexedPropertiesWithIndexedCustomEditorForProperty() { IndexedTestBean bean = new IndexedTestBean(); TestBean tb0 = bean.getArray()[0]; TestBean tb1 = bean.getArray()[1]; @@ -1178,7 +1181,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testIndexedPropertiesWithDirectAccessAndPropertyEditors() { + void indexedPropertiesWithDirectAccessAndPropertyEditors() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(TestBean.class, "array", new PropertyEditorSupport() { @@ -1232,7 +1235,7 @@ public String getAsText() { } @Test - void testIndexedPropertiesWithDirectAccessAndSpecificPropertyEditors() { + void indexedPropertiesWithDirectAccessAndSpecificPropertyEditors() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(TestBean.class, "array[0]", new PropertyEditorSupport() { @@ -1319,7 +1322,8 @@ public String getAsText() { } @Test - void testIndexedPropertiesWithListPropertyEditor() { + @SuppressWarnings("unchecked") + void indexedPropertiesWithListPropertyEditor() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); bw.registerCustomEditor(List.class, "list", new PropertyEditorSupport() { @@ -1333,11 +1337,11 @@ public void setAsText(String text) throws IllegalArgumentException { bw.setPropertyValue("list", "1"); assertThat(((TestBean) bean.getList().get(0)).getName()).isEqualTo("list1"); bw.setPropertyValue("list[0]", "test"); - assertThat(bean.getList().get(0)).isEqualTo("test"); + assertThat(bean.getList()).singleElement().isEqualTo("test"); } @Test - void testConversionToOldCollections() throws PropertyVetoException { + void conversionToOldCollections() { OldCollectionsBean tb = new OldCollectionsBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(Vector.class, new CustomCollectionEditor(Vector.class)); @@ -1345,8 +1349,8 @@ void testConversionToOldCollections() throws PropertyVetoException { bw.setPropertyValue("vector", new String[] {"a", "b"}); assertThat(tb.getVector()).hasSize(2); - assertThat(tb.getVector().get(0)).isEqualTo("a"); - assertThat(tb.getVector().get(1)).isEqualTo("b"); + assertThat(tb.getVector()).element(0).isEqualTo("a"); + assertThat(tb.getVector()).element(1).isEqualTo("b"); bw.setPropertyValue("hashtable", Collections.singletonMap("foo", "bar")); assertThat(tb.getHashtable()).hasSize(1); @@ -1354,7 +1358,8 @@ void testConversionToOldCollections() throws PropertyVetoException { } @Test - void testUninitializedArrayPropertyWithCustomEditor() { + @SuppressWarnings("unchecked") + void uninitializedArrayPropertyWithCustomEditor() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); PropertyEditor pe = new CustomNumberEditor(Integer.class, true); @@ -1362,7 +1367,7 @@ void testUninitializedArrayPropertyWithCustomEditor() { TestBean tb = new TestBean(); bw.setPropertyValue("list", new ArrayList<>()); bw.setPropertyValue("list[0]", tb); - assertThat(bean.getList().get(0)).isEqualTo(tb); + assertThat(bean.getList()).element(0).isEqualTo(tb); assertThat(bw.findCustomEditor(int.class, "list.age")).isEqualTo(pe); assertThat(bw.findCustomEditor(null, "list.age")).isEqualTo(pe); assertThat(bw.findCustomEditor(int.class, "list[0].age")).isEqualTo(pe); @@ -1370,7 +1375,7 @@ void testUninitializedArrayPropertyWithCustomEditor() { } @Test - void testArrayToArrayConversion() throws PropertyVetoException { + void arrayToArrayConversion() { IndexedTestBean tb = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(TestBean.class, new PropertyEditorSupport() { @@ -1386,7 +1391,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testArrayToStringConversion() throws PropertyVetoException { + void arrayToStringConversion() { TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.registerCustomEditor(String.class, new PropertyEditorSupport() { @@ -1400,7 +1405,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testClassArrayEditorSunnyDay() { + void classArrayEditorSunnyDay() { ClassArrayEditor classArrayEditor = new ClassArrayEditor(); classArrayEditor.setAsText("java.lang.String,java.util.HashMap"); Class[] classes = (Class[]) classArrayEditor.getValue(); @@ -1413,7 +1418,7 @@ void testClassArrayEditorSunnyDay() { } @Test - void testClassArrayEditorSunnyDayWithArrayTypes() { + void classArrayEditorSunnyDayWithArrayTypes() { ClassArrayEditor classArrayEditor = new ClassArrayEditor(); classArrayEditor.setAsText("java.lang.String[],java.util.Map[],int[],float[][][]"); Class[] classes = (Class[]) classArrayEditor.getValue(); @@ -1428,7 +1433,7 @@ void testClassArrayEditorSunnyDayWithArrayTypes() { } @Test - void testClassArrayEditorSetAsTextWithNull() { + void classArrayEditorSetAsTextWithNull() { ClassArrayEditor editor = new ClassArrayEditor(); editor.setAsText(null); assertThat(editor.getValue()).isNull(); @@ -1436,7 +1441,7 @@ void testClassArrayEditorSetAsTextWithNull() { } @Test - void testClassArrayEditorSetAsTextWithEmptyString() { + void classArrayEditorSetAsTextWithEmptyString() { ClassArrayEditor editor = new ClassArrayEditor(); editor.setAsText(""); assertThat(editor.getValue()).isNull(); @@ -1444,7 +1449,7 @@ void testClassArrayEditorSetAsTextWithEmptyString() { } @Test - void testClassArrayEditorSetAsTextWithWhitespaceString() { + void classArrayEditorSetAsTextWithWhitespaceString() { ClassArrayEditor editor = new ClassArrayEditor(); editor.setAsText("\n"); assertThat(editor.getValue()).isNull(); @@ -1452,7 +1457,7 @@ void testClassArrayEditorSetAsTextWithWhitespaceString() { } @Test - void testCharsetEditor() { + void charsetEditor() { CharsetEditor editor = new CharsetEditor(); String name = "UTF-8"; editor.setAsText(name); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java index 3076977e9ec4..84d6eff38126 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.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. @@ -31,54 +31,64 @@ * @author Chris Beams * @author Juergen Hoeller */ -public class FileEditorTests { +class FileEditorTests { @Test - public void testClasspathFileName() { + void testClasspathFileName() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).exists(); } @Test - public void testWithNonExistentResource() { - PropertyEditor propertyEditor = new FileEditor(); + void testWithNonExistentResource() { + PropertyEditor fileEditor = new FileEditor(); assertThatIllegalArgumentException().isThrownBy(() -> - propertyEditor.setAsText("classpath:no_way_this_file_is_found.doc")); + fileEditor.setAsText("classpath:no_way_this_file_is_found.doc")); } @Test - public void testWithNonExistentFile() { + void testWithNonExistentFile() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("file:no_way_this_file_is_found.doc"); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).doesNotExist(); } @Test - public void testAbsoluteFileName() { + void testAbsoluteFileName() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("/no_way_this_file_is_found.doc"); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).doesNotExist(); } @Test - public void testUnqualifiedFileNameFound() { + void testCurrentDirectory() { + PropertyEditor fileEditor = new FileEditor(); + fileEditor.setAsText("file:."); + Object value = fileEditor.getValue(); + assertThat(value).isInstanceOf(File.class); + File file = (File) value; + assertThat(file).isEqualTo(new File(".")); + } + + @Test + void testUnqualifiedFileNameFound() { PropertyEditor fileEditor = new FileEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; fileEditor.setAsText(fileName); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).exists(); String absolutePath = file.getAbsolutePath().replace('\\', '/'); @@ -86,13 +96,13 @@ public void testUnqualifiedFileNameFound() { } @Test - public void testUnqualifiedFileNameNotFound() { + void testUnqualifiedFileNameNotFound() { PropertyEditor fileEditor = new FileEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".clazz"; fileEditor.setAsText(fileName); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).doesNotExist(); String absolutePath = file.getAbsolutePath().replace('\\', '/'); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java index d0a3e676ecf6..fe31766d9bb3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.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,21 +27,21 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the {@link InputStreamEditor} class. + * Tests for {@link InputStreamEditor}. * * @author Rick Evans * @author Chris Beams */ -public class InputStreamEditorTests { +class InputStreamEditorTests { @Test - public void testCtorWithNullResourceEditor() { + void testCtorWithNullResourceEditor() { assertThatIllegalArgumentException().isThrownBy(() -> new InputStreamEditor(null)); } @Test - public void testSunnyDay() throws IOException { + void testSunnyDay() throws IOException { InputStream stream = null; try { String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + @@ -50,7 +50,7 @@ public void testSunnyDay() throws IOException { editor.setAsText(resource); Object value = editor.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof InputStream).isTrue(); + assertThat(value).isInstanceOf(InputStream.class); stream = (InputStream) value; assertThat(stream.available()).isGreaterThan(0); } @@ -62,14 +62,14 @@ public void testSunnyDay() throws IOException { } @Test - public void testWhenResourceDoesNotExist() { + void testWhenResourceDoesNotExist() { InputStreamEditor editor = new InputStreamEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText("classpath:bingo!")); } @Test - public void testGetAsTextReturnsNullByDefault() { + void testGetAsTextReturnsNullByDefault() { assertThat(new InputStreamEditor().getAsText()).isNull(); String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java index d55cc18d48a4..ed4058b4fe53 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.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,7 @@ import java.beans.PropertyEditor; import java.io.File; import java.nio.file.Path; +import java.nio.file.Paths; import org.junit.jupiter.api.Test; @@ -31,10 +32,10 @@ * @author Juergen Hoeller * @since 4.3.2 */ -public class PathEditorTests { +class PathEditorTests { @Test - public void testClasspathPathName() { + void testClasspathPathName() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); @@ -45,14 +46,14 @@ public void testClasspathPathName() { } @Test - public void testWithNonExistentResource() { - PropertyEditor propertyEditor = new PathEditor(); + void testWithNonExistentResource() { + PropertyEditor pathEditor = new PathEditor(); assertThatIllegalArgumentException().isThrownBy(() -> - propertyEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); + pathEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); } @Test - public void testWithNonExistentPath() { + void testWithNonExistentPath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("file:/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); @@ -62,7 +63,7 @@ public void testWithNonExistentPath() { } @Test - public void testAbsolutePath() { + void testAbsolutePath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); @@ -72,7 +73,7 @@ public void testAbsolutePath() { } @Test - public void testWindowsAbsolutePath() { + void testWindowsAbsolutePath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("C:\\no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); @@ -82,7 +83,7 @@ public void testWindowsAbsolutePath() { } @Test - public void testWindowsAbsoluteFilePath() { + void testWindowsAbsoluteFilePath() { PropertyEditor pathEditor = new PathEditor(); try { pathEditor.setAsText("file://C:\\no_way_this_file_is_found.doc"); @@ -99,7 +100,17 @@ public void testWindowsAbsoluteFilePath() { } @Test - public void testUnqualifiedPathNameFound() { + void testCurrentDirectory() { + PropertyEditor pathEditor = new PathEditor(); + pathEditor.setAsText("file:."); + Object value = pathEditor.getValue(); + assertThat(value).isInstanceOf(Path.class); + Path path = (Path) value; + assertThat(path).isEqualTo(Paths.get(".")); + } + + @Test + void testUnqualifiedPathNameFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; @@ -117,7 +128,7 @@ public void testUnqualifiedPathNameFound() { } @Test - public void testUnqualifiedPathNameNotFound() { + void testUnqualifiedPathNameNotFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".clazz"; diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java index b3ec8b4be28b..3671fa3d0281 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.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,6 +22,7 @@ import org.junit.jupiter.api.Test; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; /** @@ -32,10 +33,10 @@ * @author Juergen Hoeller * @author Rick Evans */ -public class PropertiesEditorTests { +class PropertiesEditorTests { @Test - public void oneProperty() { + void oneProperty() { String s = "foo=bar"; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); @@ -45,7 +46,7 @@ public void oneProperty() { } @Test - public void twoProperties() { + void twoProperties() { String s = "foo=bar with whitespace\n" + "me=mi"; PropertiesEditor pe= new PropertiesEditor(); @@ -57,10 +58,11 @@ public void twoProperties() { } @Test - public void handlesEqualsInValue() { - String s = "foo=bar\n" + - "me=mi\n" + - "x=y=z"; + void handlesEqualsInValue() { + String s = """ + foo=bar + me=mi + x=y=z"""; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); Properties p = (Properties) pe.getValue(); @@ -71,7 +73,7 @@ public void handlesEqualsInValue() { } @Test - public void handlesEmptyProperty() { + void handlesEmptyProperty() { String s = "foo=bar\nme=mi\nx="; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); @@ -83,7 +85,7 @@ public void handlesEmptyProperty() { } @Test - public void handlesEmptyPropertyWithoutEquals() { + void handlesEmptyPropertyWithoutEquals() { String s = "foo\nme=mi\nx=x"; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); @@ -97,13 +99,15 @@ public void handlesEmptyPropertyWithoutEquals() { * Comments begin with # */ @Test - public void ignoresCommentLinesAndEmptyLines() { - String s = "#Ignore this comment\n" + - "foo=bar\n" + - "#Another=comment more junk /\n" + - "me=mi\n" + - "x=x\n" + - "\n"; + void ignoresCommentLinesAndEmptyLines() { + String s = """ + #Ignore this comment + foo=bar + #Another=comment more junk / + me=mi + x=x + + """; PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); Properties p = (Properties) pe.getValue(); @@ -119,7 +123,7 @@ public void ignoresCommentLinesAndEmptyLines() { * still ignored: The standard syntax doesn't allow this on JDK 1.3. */ @Test - public void ignoresLeadingSpacesAndTabs() { + void ignoresLeadingSpacesAndTabs() { String s = " #Ignore this comment\n" + "\t\tfoo=bar\n" + "\t#Another comment more junk \n" + @@ -129,13 +133,12 @@ public void ignoresLeadingSpacesAndTabs() { PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(s); Properties p = (Properties) pe.getValue(); - assertThat(p.size()).as("contains 3 entries, not " + p.size()).isEqualTo(3); - assertThat(p.get("foo").equals("bar")).as("foo is bar").isTrue(); - assertThat(p.get("me").equals("mi")).as("me=mi").isTrue(); + assertThat(p).contains(entry("foo", "bar"), entry("me", "mi")); + assertThat(p).hasSize(3); } @Test - public void nullValue() { + void nullValue() { PropertiesEditor pe= new PropertiesEditor(); pe.setAsText(null); Properties p = (Properties) pe.getValue(); @@ -143,7 +146,7 @@ public void nullValue() { } @Test - public void emptyString() { + void emptyString() { PropertiesEditor pe = new PropertiesEditor(); pe.setAsText(""); Properties p = (Properties) pe.getValue(); @@ -151,7 +154,7 @@ public void emptyString() { } @Test - public void usingMapAsValueSource() { + void usingMapAsValueSource() { Map map = new HashMap<>(); map.put("one", "1"); map.put("two", "2"); @@ -160,7 +163,7 @@ public void usingMapAsValueSource() { pe.setValue(map); Object value = pe.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof Properties).isTrue(); + assertThat(value).isInstanceOf(Properties.class); Properties props = (Properties) value; assertThat(props).hasSize(3); assertThat(props.getProperty("one")).isEqualTo("1"); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java index 79e48a5db085..3425e825bb13 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.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,21 +27,21 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the {@link ReaderEditor} class. + * Tests for {@link ReaderEditor}. * * @author Juergen Hoeller * @since 4.2 */ -public class ReaderEditorTests { +class ReaderEditorTests { @Test - public void testCtorWithNullResourceEditor() { + void testCtorWithNullResourceEditor() { assertThatIllegalArgumentException().isThrownBy(() -> new ReaderEditor(null)); } @Test - public void testSunnyDay() throws IOException { + void testSunnyDay() throws IOException { Reader reader = null; try { String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + @@ -50,7 +50,7 @@ public void testSunnyDay() throws IOException { editor.setAsText(resource); Object value = editor.getValue(); assertThat(value).isNotNull(); - assertThat(value instanceof Reader).isTrue(); + assertThat(value).isInstanceOf(Reader.class); reader = (Reader) value; assertThat(reader.ready()).isTrue(); } @@ -62,7 +62,7 @@ public void testSunnyDay() throws IOException { } @Test - public void testWhenResourceDoesNotExist() { + void testWhenResourceDoesNotExist() { String resource = "classpath:bingo!"; ReaderEditor editor = new ReaderEditor(); assertThatIllegalArgumentException().isThrownBy(() -> @@ -70,7 +70,7 @@ public void testWhenResourceDoesNotExist() { } @Test - public void testGetAsTextReturnsNullByDefault() { + void testGetAsTextReturnsNullByDefault() { assertThat(new ReaderEditor().getAsText()).isNull(); String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java index 4cbdef1ddc4f..7fb84bca01c8 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.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. @@ -24,12 +24,12 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Unit tests for the {@link ResourceBundleEditor} class. + * Tests for {@link ResourceBundleEditor}. * * @author Rick Evans * @author Chris Beams */ -public class ResourceBundleEditorTests { +class ResourceBundleEditorTests { private static final String BASE_NAME = ResourceBundleEditorTests.class.getName(); @@ -37,7 +37,7 @@ public class ResourceBundleEditorTests { @Test - public void testSetAsTextWithJustBaseName() { + void testSetAsTextWithJustBaseName() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME); Object value = editor.getValue(); @@ -49,7 +49,7 @@ public void testSetAsTextWithJustBaseName() { } @Test - public void testSetAsTextWithBaseNameThatEndsInDefaultSeparator() { + void testSetAsTextWithBaseNameThatEndsInDefaultSeparator() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME + "_"); Object value = editor.getValue(); @@ -61,7 +61,7 @@ public void testSetAsTextWithBaseNameThatEndsInDefaultSeparator() { } @Test - public void testSetAsTextWithBaseNameAndLanguageCode() { + void testSetAsTextWithBaseNameAndLanguageCode() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME + "Lang" + "_en"); Object value = editor.getValue(); @@ -73,7 +73,7 @@ public void testSetAsTextWithBaseNameAndLanguageCode() { } @Test - public void testSetAsTextWithBaseNameLanguageAndCountryCode() { + void testSetAsTextWithBaseNameLanguageAndCountryCode() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME + "LangCountry" + "_en_GB"); Object value = editor.getValue(); @@ -85,7 +85,7 @@ public void testSetAsTextWithBaseNameLanguageAndCountryCode() { } @Test - public void testSetAsTextWithTheKitchenSink() { + void testSetAsTextWithTheKitchenSink() { ResourceBundleEditor editor = new ResourceBundleEditor(); editor.setAsText(BASE_NAME + "LangCountryDialect" + "_en_GB_GLASGOW"); Object value = editor.getValue(); @@ -97,28 +97,28 @@ public void testSetAsTextWithTheKitchenSink() { } @Test - public void testSetAsTextWithNull() { + void testSetAsTextWithNull() { ResourceBundleEditor editor = new ResourceBundleEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText(null)); } @Test - public void testSetAsTextWithEmptyString() { + void testSetAsTextWithEmptyString() { ResourceBundleEditor editor = new ResourceBundleEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText("")); } @Test - public void testSetAsTextWithWhiteSpaceString() { + void testSetAsTextWithWhiteSpaceString() { ResourceBundleEditor editor = new ResourceBundleEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText(" ")); } @Test - public void testSetAsTextWithJustSeparatorString() { + void testSetAsTextWithJustSeparatorString() { ResourceBundleEditor editor = new ResourceBundleEditor(); assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText("_")); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java index 4d215e015661..18c28234c21f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.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. @@ -29,75 +29,75 @@ * @author Juergen Hoeller * @author Arjen Poutsma */ -public class URIEditorTests { +class URIEditorTests { @Test - public void standardURI() { + void standardURI() { doTestURI("mailto:juergen.hoeller@interface21.com"); } @Test - public void withNonExistentResource() { + void withNonExistentResource() { doTestURI("gonna:/freak/in/the/morning/freak/in/the.evening"); } @Test - public void standardURL() { + void standardURL() { doTestURI("https://www.springframework.org"); } @Test - public void standardURLWithFragment() { + void standardURLWithFragment() { doTestURI("https://www.springframework.org#1"); } @Test - public void standardURLWithWhitespace() { + void standardURLWithWhitespace() { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText(" https://www.springframework.org "); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uri.toString()).isEqualTo("https://www.springframework.org"); } @Test - public void classpathURL() { + void classpathURL() { PropertyEditor uriEditor = new URIEditor(getClass().getClassLoader()); uriEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.getScheme()).doesNotStartWith("classpath"); } @Test - public void classpathURLWithWhitespace() { + void classpathURLWithWhitespace() { PropertyEditor uriEditor = new URIEditor(getClass().getClassLoader()); uriEditor.setAsText(" classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class "); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.getScheme()).doesNotStartWith("classpath"); } @Test - public void classpathURLAsIs() { + void classpathURLAsIs() { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText("classpath:test.txt"); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.getScheme()).startsWith("classpath"); } @Test - public void setAsTextWithNull() { + void setAsTextWithNull() { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText(null); assertThat(uriEditor.getValue()).isNull(); @@ -105,28 +105,28 @@ public void setAsTextWithNull() { } @Test - public void getAsTextReturnsEmptyStringIfValueNotSet() { + void getAsTextReturnsEmptyStringIfValueNotSet() { PropertyEditor uriEditor = new URIEditor(); assertThat(uriEditor.getAsText()).isEmpty(); } @Test - public void encodeURI() { + void encodeURI() { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText("https://example.com/spaces and \u20AC"); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.toASCIIString()).isEqualTo("https://example.com/spaces%20and%20%E2%82%AC"); } @Test - public void encodeAlreadyEncodedURI() { + void encodeAlreadyEncodedURI() { PropertyEditor uriEditor = new URIEditor(false); uriEditor.setAsText("https://example.com/spaces%20and%20%E2%82%AC"); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); assertThat(uri.toASCIIString()).isEqualTo("https://example.com/spaces%20and%20%E2%82%AC"); @@ -137,7 +137,7 @@ private void doTestURI(String uriSpec) { PropertyEditor uriEditor = new URIEditor(); uriEditor.setAsText(uriSpec); Object value = uriEditor.getValue(); - assertThat(value instanceof URI).isTrue(); + assertThat(value).isInstanceOf(URI.class); URI uri = (URI) value; assertThat(uri.toString()).isEqualTo(uriSpec); } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java index ba96e1b9ee83..4b63b6e7f60b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.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. @@ -30,55 +30,55 @@ * @author Rick Evans * @author Chris Beams */ -public class URLEditorTests { +class URLEditorTests { @Test - public void testCtorWithNullResourceEditor() { + void testCtorWithNullResourceEditor() { assertThatIllegalArgumentException().isThrownBy(() -> new URLEditor(null)); } @Test - public void testStandardURI() { + void testStandardURI() { PropertyEditor urlEditor = new URLEditor(); urlEditor.setAsText("mailto:juergen.hoeller@interface21.com"); Object value = urlEditor.getValue(); - assertThat(value instanceof URL).isTrue(); + assertThat(value).isInstanceOf(URL.class); URL url = (URL) value; assertThat(urlEditor.getAsText()).isEqualTo(url.toExternalForm()); } @Test - public void testStandardURL() { + void testStandardURL() { PropertyEditor urlEditor = new URLEditor(); urlEditor.setAsText("https://www.springframework.org"); Object value = urlEditor.getValue(); - assertThat(value instanceof URL).isTrue(); + assertThat(value).isInstanceOf(URL.class); URL url = (URL) value; assertThat(urlEditor.getAsText()).isEqualTo(url.toExternalForm()); } @Test - public void testClasspathURL() { + void testClasspathURL() { PropertyEditor urlEditor = new URLEditor(); urlEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = urlEditor.getValue(); - assertThat(value instanceof URL).isTrue(); + assertThat(value).isInstanceOf(URL.class); URL url = (URL) value; assertThat(urlEditor.getAsText()).isEqualTo(url.toExternalForm()); assertThat(url.getProtocol()).doesNotStartWith("classpath"); } @Test - public void testWithNonExistentResource() { + void testWithNonExistentResource() { PropertyEditor urlEditor = new URLEditor(); assertThatIllegalArgumentException().isThrownBy(() -> urlEditor.setAsText("gonna:/freak/in/the/morning/freak/in/the.evening")); } @Test - public void testSetAsTextWithNull() { + void testSetAsTextWithNull() { PropertyEditor urlEditor = new URLEditor(); urlEditor.setAsText(null); assertThat(urlEditor.getValue()).isNull(); @@ -86,7 +86,7 @@ public void testSetAsTextWithNull() { } @Test - public void testGetAsTextReturnsEmptyStringIfValueNotSet() { + void testGetAsTextReturnsEmptyStringIfValueNotSet() { PropertyEditor urlEditor = new URLEditor(); assertThat(urlEditor.getAsText()).isEmpty(); } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java index 535f0ed3ad87..932a78fdbdf1 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.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,10 +23,12 @@ import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * @author Nicholas Williams * @author Sam Brannen + * @author Juergen Hoeller */ class ZoneIdEditorTests { @@ -69,4 +71,9 @@ void getValueAsText() { assertThat(editor.getAsText()).as("The text version is not correct.").isEqualTo("America/New_York"); } + @Test + void correctExceptionForInvalid() { + assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText("INVALID")).withMessageContaining("INVALID"); + } + } 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 876233fb3f2a..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 @@ -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,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; @@ -32,7 +32,7 @@ * @author Chris Beams * @since 20.05.2003 */ -public class PagedListHolderTests { +class PagedListHolderTests { @Test @SuppressWarnings({ "rawtypes", "unchecked" }) diff --git a/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java b/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java index aac0b2097b0d..0bf4a4758eda 100644 --- a/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.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,15 +23,15 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link PropertyComparator}. + * Tests for {@link PropertyComparator}. * * @author Keith Donald * @author Chris Beams */ -public class PropertyComparatorTests { +class PropertyComparatorTests { @Test - public void testPropertyComparator() { + void testPropertyComparator() { Dog dog = new Dog(); dog.setNickName("mace"); @@ -45,7 +45,7 @@ public void testPropertyComparator() { } @Test - public void testPropertyComparatorNulls() { + void testPropertyComparatorNulls() { Dog dog = new Dog(); Dog dog2 = new Dog(); PropertyComparator c = new PropertyComparator<>("nickName", false, true); @@ -53,7 +53,7 @@ public void testPropertyComparatorNulls() { } @Test - public void testChainedComparators() { + void testChainedComparators() { Comparator c = new PropertyComparator<>("lastName", false, true); Dog dog1 = new Dog(); @@ -74,7 +74,7 @@ public void testChainedComparators() { } @Test - public void testChainedComparatorsReversed() { + void testChainedComparatorsReversed() { Comparator c = (new PropertyComparator("lastName", false, true)). thenComparing(new PropertyComparator<>("firstName", false, true)); diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt index f9ba1dafa16d..6bb5b3936338 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt @@ -21,7 +21,6 @@ import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.Test import org.springframework.core.ResolvableType -import kotlin.reflect.full.createInstance /** * Mock object based tests for BeanFactory Kotlin extensions. 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 6623bb4581fc..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 @@ -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,10 +22,8 @@ import org.junit.jupiter.api.Test import org.springframework.aot.hint.* import org.springframework.aot.test.generate.TestGenerationContext import org.springframework.beans.factory.config.BeanDefinition -import org.springframework.beans.factory.support.DefaultListableBeanFactory -import org.springframework.beans.factory.support.InstanceSupplier -import org.springframework.beans.factory.support.RegisteredBean -import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.beans.factory.support.* +import org.springframework.beans.testfixture.beans.KotlinConfiguration import org.springframework.beans.testfixture.beans.KotlinTestBean import org.springframework.beans.testfixture.beans.KotlinTestBeanWithOptionalParameter import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder @@ -45,99 +43,138 @@ import javax.lang.model.element.Modifier */ class InstanceSupplierCodeGeneratorKotlinTests { - private val generationContext = TestGenerationContext() - - @Test - fun generateWhenHasDefaultConstructor() { - val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java) - val beanFactory = DefaultListableBeanFactory() - compile(beanFactory, beanDefinition) { instanceSupplier, compiled -> - val bean = getBean(beanFactory, beanDefinition, instanceSupplier) - 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)) - } - - @Test - fun generateWhenConstructorHasOptionalParameter() { - val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java) - val beanFactory = DefaultListableBeanFactory() - compile(beanFactory, beanDefinition) { instanceSupplier, compiled -> - val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier) - Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java) - Assertions.assertThat(compiled.sourceFile) - .contains("return BeanInstanceSupplier.forConstructor();") - } - Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java)) - .satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) - } - - private fun getReflectionHints(): ReflectionHints { - return generationContext.runtimeHints.reflection() - } - - private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer { - return ThrowingConsumer { - Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode)) - } - } - - private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer { - return ThrowingConsumer { - Assertions.assertThat(it.memberCategories).contains(category) - } - } - - private fun hasMode(mode: ExecutableMode): ThrowingConsumer { - return ThrowingConsumer { - Assertions.assertThat(it.mode).isEqualTo(mode) - } - } - - @Suppress("UNCHECKED_CAST") - private fun getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition, - instanceSupplier: InstanceSupplier<*>): T { - (beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier - beanFactory.registerBeanDefinition("testBean", beanDefinition) - return beanFactory.getBean("testBean") as T - } - - private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition, - result: BiConsumer, Compiled>) { - - val freshBeanFactory = DefaultListableBeanFactory(beanFactory) - freshBeanFactory.registerBeanDefinition("testBean", beanDefinition) - val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean") - val typeBuilder = DeferredTypeBuilder() - val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder) - val generator = InstanceSupplierCodeGenerator( - generationContext, generateClass.name, - generateClass.methods, false - ) - val constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod() - Assertions.assertThat(constructorOrFactoryMethod).isNotNull() - val generatedCode = generator.generateCode(registeredBean, constructorOrFactoryMethod) - typeBuilder.set { type: TypeSpec.Builder -> - type.addModifiers(Modifier.PUBLIC) - type.addSuperinterface( - ParameterizedTypeName.get( - Supplier::class.java, - InstanceSupplier::class.java - ) - ) - type.addMethod( - MethodSpec.methodBuilder("get") - .addModifiers(Modifier.PUBLIC) - .returns(InstanceSupplier::class.java) - .addStatement("return \$L", generatedCode).build() - ) - } - generationContext.writeGeneratedContent() - TestCompiler.forSystem().with(generationContext).compile { - result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it) - } - } + private val generationContext = TestGenerationContext() + + private val beanFactory = DefaultListableBeanFactory() + + @Test + fun generateWhenHasDefaultConstructor() { + val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java) + val beanFactory = DefaultListableBeanFactory() + compile(beanFactory, beanDefinition) { instanceSupplier, compiled -> + val bean = getBean(beanFactory, beanDefinition, instanceSupplier) + Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java) + Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)") + } + Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java)).isNotNull + } + + @Test + fun generateWhenConstructorHasOptionalParameter() { + val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java) + val beanFactory = DefaultListableBeanFactory() + compile(beanFactory, beanDefinition) { instanceSupplier, compiled -> + val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier) + Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java) + Assertions.assertThat(compiled.sourceFile) + .contains("return BeanInstanceSupplier.forConstructor();") + } + Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java)) + .satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) + } + + @Test + fun generateWhenHasFactoryMethodWithNoArg() { + val beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(String::class.java) + .setFactoryMethodOnBean("stringBean", "config").beanDefinition + this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(KotlinConfiguration::class.java).beanDefinition + ) + compile(beanFactory, beanDefinition) { instanceSupplier, compiled -> + val bean = getBean(beanFactory, beanDefinition, instanceSupplier) + Assertions.assertThat(bean).isInstanceOf(String::class.java) + Assertions.assertThat(bean).isEqualTo("Hello") + Assertions.assertThat(compiled.sourceFile).contains( + "getBeanFactory().getBean(\"config\", KotlinConfiguration.class).stringBean()" + ) + } + Assertions.assertThat(getReflectionHints().getTypeHint(KotlinConfiguration::class.java)).isNotNull + } + + @Test + fun generateWhenHasSuspendingFactoryMethod() { + val beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(String::class.java) + .setFactoryMethodOnBean("suspendingStringBean", "config").beanDefinition + this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(KotlinConfiguration::class.java).beanDefinition + ) + Assertions.assertThatExceptionOfType(AotBeanProcessingException::class.java).isThrownBy { + compile(beanFactory, beanDefinition) { _, _ -> } + } + } + + private fun getReflectionHints(): ReflectionHints { + return generationContext.runtimeHints.reflection() + } + + private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer { + return ThrowingConsumer { + Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode)) + } + } + + private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer { + return ThrowingConsumer { + Assertions.assertThat(it.memberCategories).contains(category) + } + } + + private fun hasMode(mode: ExecutableMode): ThrowingConsumer { + return ThrowingConsumer { + Assertions.assertThat(it.mode).isEqualTo(mode) + } + } + + private fun hasMethodWithMode(mode: ExecutableMode): ThrowingConsumer { + return ThrowingConsumer { hint: TypeHint -> + Assertions.assertThat(hint.methods()).anySatisfy(hasMode(mode)) + } + } + + @Suppress("UNCHECKED_CAST") + private fun getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition, + instanceSupplier: InstanceSupplier<*>): T { + (beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier + beanFactory.registerBeanDefinition("testBean", beanDefinition) + return beanFactory.getBean("testBean") as T + } + + private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition, + result: BiConsumer, Compiled>) { + + val freshBeanFactory = DefaultListableBeanFactory(beanFactory) + freshBeanFactory.registerBeanDefinition("testBean", beanDefinition) + val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean") + val typeBuilder = DeferredTypeBuilder() + val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder) + val generator = InstanceSupplierCodeGenerator( + generationContext, generateClass.name, + generateClass.methods, false + ) + val instantiationDescriptor = registeredBean.resolveInstantiationDescriptor() + Assertions.assertThat(instantiationDescriptor).isNotNull() + val generatedCode = generator.generateCode(registeredBean, instantiationDescriptor) + typeBuilder.set { type: TypeSpec.Builder -> + type.addModifiers(Modifier.PUBLIC) + type.addSuperinterface( + ParameterizedTypeName.get( + Supplier::class.java, + InstanceSupplier::class.java + ) + ) + type.addMethod( + MethodSpec.methodBuilder("get") + .addModifiers(Modifier.PUBLIC) + .returns(InstanceSupplier::class.java) + .addStatement("return \$L", generatedCode).build() + ) + } + generationContext.writeGeneratedContent() + TestCompiler.forSystem().with(generationContext).compile { + result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it) + } + } } diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/support/ConstructorResolverKotlinTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/support/ConstructorResolverKotlinTests.kt new file mode 100644 index 000000000000..4aa8bb9d7c1b --- /dev/null +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/support/ConstructorResolverKotlinTests.kt @@ -0,0 +1,55 @@ +/* + * 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.beans.factory.support + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.BeanWrapper +import org.springframework.beans.factory.BeanCreationException +import org.springframework.beans.factory.config.BeanDefinition +import org.springframework.beans.testfixture.beans.factory.generator.factory.KotlinFactory + +class ConstructorResolverKotlinTests { + + @Test + fun instantiateBeanInstanceWithBeanClassAndFactoryMethodName() { + val beanFactory = DefaultListableBeanFactory() + val beanDefinition: BeanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(KotlinFactory::class.java).setFactoryMethod("create") + .beanDefinition + val beanWrapper = instantiate(beanFactory, beanDefinition) + Assertions.assertThat(beanWrapper.wrappedInstance).isEqualTo("test") + } + + @Test + fun instantiateBeanInstanceWithBeanClassAndSuspendingFactoryMethodName() { + val beanFactory = DefaultListableBeanFactory() + val beanDefinition: BeanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(KotlinFactory::class.java).setFactoryMethod("suspendingCreate") + .beanDefinition + Assertions.assertThatThrownBy { instantiate(beanFactory, beanDefinition, null) } + .isInstanceOf(BeanCreationException::class.java) + .hasMessageContaining("suspending functions are not supported") + + } + + private fun instantiate(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition, + vararg explicitArgs: Any?): BeanWrapper { + return ConstructorResolver(beanFactory) + .instantiateUsingFactoryMethod("testBean", (beanDefinition as RootBeanDefinition), explicitArgs) + } +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java index 2ad5ece17f5e..83d3755b5128 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,12 +23,12 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.springframework.core.io.Resource; +import org.springframework.util.CollectionUtils; /** * @author Juergen Hoeller @@ -267,9 +267,9 @@ public Set getCustomEnumSetMismatch() { } public void setCustomEnumSetMismatch(Set customEnumSet) { - this.customEnumSet = new HashSet<>(customEnumSet.size()); - for (Iterator iterator = customEnumSet.iterator(); iterator.hasNext(); ) { - this.customEnumSet.add(CustomEnum.valueOf(iterator.next())); + this.customEnumSet = CollectionUtils.newHashSet(customEnumSet.size()); + for (String customEnumName : customEnumSet) { + this.customEnumSet.add(CustomEnum.valueOf(customEnumName)); } } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java index 43ce2dd40ed8..1fa63057745d 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,6 +33,10 @@ public interface ITestBean extends AgeHolder { void setName(String name); + default void applyName(Object name) { + setName(String.valueOf(name)); + } + ITestBean getSpouse(); void setSpouse(ITestBean spouse); diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java index 54d32af54535..39026aec6f95 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -49,6 +50,8 @@ public class IndexedTestBean { private SortedMap sortedMap; + private IterableMap iterableMap; + private MyTestBeans myTestBeans; @@ -73,6 +76,9 @@ public void populate() { TestBean tb6 = new TestBean("name6", 0); TestBean tb7 = new TestBean("name7", 0); TestBean tb8 = new TestBean("name8", 0); + TestBean tbA = new TestBean("nameA", 0); + TestBean tbB = new TestBean("nameB", 0); + TestBean tbC = new TestBean("nameC", 0); TestBean tbX = new TestBean("nameX", 0); TestBean tbY = new TestBean("nameY", 0); TestBean tbZ = new TestBean("nameZ", 0); @@ -88,6 +94,12 @@ public void populate() { this.map.put("key2", tb5); this.map.put("key.3", tb5); List list = new ArrayList(); + list.add(tbA); + list.add(tbB); + this.iterableMap = new IterableMap<>(); + this.iterableMap.put("key1", tbC); + this.iterableMap.put("key2", list); + list = new ArrayList(); list.add(tbX); list.add(tbY); this.map.put("key4", list); @@ -152,6 +164,14 @@ public void setSortedMap(SortedMap sortedMap) { this.sortedMap = sortedMap; } + public IterableMap getIterableMap() { + return this.iterableMap; + } + + public void setIterableMap(IterableMap iterableMap) { + this.iterableMap = iterableMap; + } + public MyTestBeans getMyTestBeans() { return myTestBeans; } @@ -161,6 +181,15 @@ public void setMyTestBeans(MyTestBeans myTestBeans) { } + @SuppressWarnings("serial") + public static class IterableMap extends LinkedHashMap implements Iterable { + + @Override + public Iterator iterator() { + return values().iterator(); + } + } + public static class MyTestBeans implements Iterable { private final Collection testBeans; diff --git a/spring-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 a77783a865c0..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 @@ -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,9 @@ package org.springframework.beans.testfixture.beans; -import org.springframework.lang.Nullable; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; /** * @author Rob Harrop @@ -34,32 +36,24 @@ public String getName() { return name; } + public void setName(String name) { + this.name = name; + } + @Override public String toString() { return getName(); } @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - final Pet pet = (Pet) o; - - if (name != null ? !name.equals(pet.name) : pet.name != null) { - return false; - } - - return true; + public boolean equals(@Nullable Object obj) { + return (this == obj) || + (obj instanceof Pet that && Objects.equals(this.name, that.name)); } @Override public int hashCode() { - return (name != null ? name.hashCode() : 0); + return (this.name != null ? this.name.hashCode() : 0); } } 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/StringFactoryBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/StringFactoryBean.java new file mode 100644 index 000000000000..65f72324536d --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/StringFactoryBean.java @@ -0,0 +1,40 @@ +/* + * 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.beans.testfixture.beans.factory; + +import org.springframework.beans.factory.FactoryBean; + +public class StringFactoryBean implements FactoryBean { + + @Override + public String getObject() { + return ""; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} + + diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/DeprecatedInjectionSamples.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/DeprecatedInjectionSamples.java new file mode 100644 index 000000000000..bb8faf0cbe00 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/annotation/DeprecatedInjectionSamples.java @@ -0,0 +1,74 @@ +/* + * 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.beans.testfixture.beans.factory.annotation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; + +public abstract class DeprecatedInjectionSamples { + + @Deprecated + public static class DeprecatedEnvironment {} + + @Deprecated + public static class DeprecatedSample { + + @Autowired + Environment environment; + + } + + public static class DeprecatedFieldInjectionPointSample { + + @Autowired + @Deprecated + Environment environment; + + } + + public static class DeprecatedFieldInjectionTypeSample { + + @Autowired + DeprecatedEnvironment environment; + } + + public static class DeprecatedPrivateFieldInjectionTypeSample { + + @Autowired + private DeprecatedEnvironment environment; + } + + public static class DeprecatedMethodInjectionPointSample { + + @Autowired + @Deprecated + void setEnvironment(Environment environment) {} + } + + public static class DeprecatedMethodInjectionTypeSample { + + @Autowired + void setEnvironment(DeprecatedEnvironment environment) {} + } + + public static class DeprecatedPrivateMethodInjectionTypeSample { + + @Autowired + private void setEnvironment(DeprecatedEnvironment environment) {} + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomBean.java new file mode 100644 index 000000000000..8bd379c95a47 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomBean.java @@ -0,0 +1,36 @@ +/* + * 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.beans.testfixture.beans.factory.aot; + +/** + * A bean that uses {@link CustomPropertyValue}. + * + * @author Stephane Nicoll + */ +public class CustomBean { + + private CustomPropertyValue customPropertyValue; + + public CustomPropertyValue getCustomPropertyValue() { + return this.customPropertyValue; + } + + public void setCustomPropertyValue(CustomPropertyValue customPropertyValue) { + this.customPropertyValue = customPropertyValue; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomPropertyValue.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomPropertyValue.java new file mode 100644 index 000000000000..f73b51bc975f --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/CustomPropertyValue.java @@ -0,0 +1,40 @@ +/* + * 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.beans.testfixture.beans.factory.aot; + +import org.springframework.aot.generate.ValueCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerator.Delegate; +import org.springframework.javapoet.CodeBlock; + +/** + * A custom value with its code generator {@link Delegate} implementation. + * + * @author Stephane Nicoll + */ +public record CustomPropertyValue(String value) { + + public static class ValueCodeGeneratorDelegate implements Delegate { + @Override + public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof CustomPropertyValue data) { + return CodeBlock.of("new $T($S)", CustomPropertyValue.class, data.value); + } + return null; + } + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DefaultSimpleBeanContract.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DefaultSimpleBeanContract.java new file mode 100644 index 000000000000..4893369a9d2b --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DefaultSimpleBeanContract.java @@ -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.beans.testfixture.beans.factory.aot; + +public class DefaultSimpleBeanContract implements SimpleBeanContract { + + public SimpleBean anotherSimpleBean() { + return new SimpleBean(); + } + +} 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/SimpleBeanContract.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanContract.java new file mode 100644 index 000000000000..96d724987407 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/SimpleBeanContract.java @@ -0,0 +1,30 @@ +/* + * 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.beans.testfixture.beans.factory.aot; + +/** + * Showcase a factory method that is defined on an interface. + * + * @author Stephane Nicoll + */ +public interface SimpleBeanContract { + + default SimpleBean simpleBean() { + return new SimpleBean(); + } + +} 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 28188b2a52a0..6f0b03c83eb2 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-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. @@ -23,14 +23,25 @@ public class InnerComponentConfiguration { public class NoDependencyComponent { public NoDependencyComponent() { - } } public class EnvironmentAwareComponent { public EnvironmentAwareComponent(Environment environment) { + } + } + public class NoDependencyComponentWithoutPublicConstructor { + + NoDependencyComponentWithoutPublicConstructor() { } } + + public class EnvironmentAwareComponentWithoutPublicConstructor { + + EnvironmentAwareComponentWithoutPublicConstructor(Environment environment) { + } + } + } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java index 89d459297bfd..17d51ed498b2 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.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,9 +20,6 @@ public class SimpleConfiguration { - public SimpleConfiguration() { - } - public String stringBean() { return "Hello"; } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedBean.java index 43ca2a4745cd..a8d90235fbf8 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedBean.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,4 +23,8 @@ */ @Deprecated public class DeprecatedBean { + + // This isn't flag deprecated on purpose + public static class Nested {} + } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalBean.java index 7f07540fb331..0d1e6b9bfae0 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalBean.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,4 +23,7 @@ */ @Deprecated(forRemoval = true) public class DeprecatedForRemovalBean { + + // This isn't flag deprecated on purpose + public static class Nested {} } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java index 4ec5151f6cdd..be9246092bfb 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java @@ -28,6 +28,7 @@ public String deprecatedString() { return "deprecated"; } + @SuppressWarnings("removal") public String deprecatedParameter(DeprecatedForRemovalBean bean) { return bean.toString(); } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java index 48da050d7076..30ab30ef7c73 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java @@ -28,10 +28,12 @@ public String deprecatedString() { return "deprecated"; } + @SuppressWarnings("deprecation") public String deprecatedParameter(DeprecatedBean bean) { return bean.toString(); } + @SuppressWarnings("deprecation") public DeprecatedBean deprecatedReturnType() { return new DeprecatedBean(); } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java index ff762788faae..6039e7d540ab 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.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,9 +16,6 @@ package org.springframework.beans.testfixture.factory.xml; -import java.beans.PropertyEditorSupport; -import java.util.StringTokenizer; - import org.junit.jupiter.api.Test; import org.springframework.beans.BeansException; @@ -53,7 +50,7 @@ public abstract class AbstractBeanFactoryTests { * Roderick bean inherits from rod, overriding name only. */ @Test - public void inheritance() { + protected void inheritance() { assertThat(getBeanFactory().containsBean("rod")).isTrue(); assertThat(getBeanFactory().containsBean("roderick")).isTrue(); TestBean rod = (TestBean) getBeanFactory().getBean("rod"); @@ -66,7 +63,7 @@ public void inheritance() { } @Test - public void getBeanWithNullArg() { + protected void getBeanWithNullArg() { assertThatIllegalArgumentException().isThrownBy(() -> getBeanFactory().getBean((String) null)); } @@ -75,7 +72,7 @@ public void getBeanWithNullArg() { * Test that InitializingBean objects receive the afterPropertiesSet() callback */ @Test - public void initializingBeanCallback() { + protected void initializingBeanCallback() { MustBeInitialized mbi = (MustBeInitialized) getBeanFactory().getBean("mustBeInitialized"); // The dummy business method will throw an exception if the // afterPropertiesSet() callback wasn't invoked @@ -87,7 +84,7 @@ public void initializingBeanCallback() { * afterPropertiesSet() callback before BeanFactoryAware callbacks */ @Test - public void lifecycleCallbacks() { + protected void lifecycleCallbacks() { LifecycleBean lb = (LifecycleBean) getBeanFactory().getBean("lifecycle"); assertThat(lb.getBeanName()).isEqualTo("lifecycle"); // The dummy business method will throw an exception if the @@ -98,24 +95,22 @@ public void lifecycleCallbacks() { } @Test - public void findsValidInstance() { + protected void findsValidInstance() { Object o = getBeanFactory().getBean("rod"); - boolean condition = o instanceof TestBean; - assertThat(condition).as("Rod bean is a TestBean").isTrue(); - TestBean rod = (TestBean) o; - assertThat(rod.getName().equals("Rod")).as("rod.name is Rod").isTrue(); - assertThat(rod.getAge()).as("rod.age is 31").isEqualTo(31); + assertThat(o).isInstanceOfSatisfying(TestBean.class, rod -> { + assertThat(rod.getName().equals("Rod")).as("rod.name is Rod").isTrue(); + assertThat(rod.getAge()).as("rod.age is 31").isEqualTo(31); + }); } @Test - public void getInstanceByMatchingClass() { + protected void getInstanceByMatchingClass() { Object o = getBeanFactory().getBean("rod", TestBean.class); - boolean condition = o instanceof TestBean; - assertThat(condition).as("Rod bean is a TestBean").isTrue(); + assertThat(o).isInstanceOf(TestBean.class); } @Test - public void getInstanceByNonmatchingClass() { + protected void getInstanceByNonmatchingClass() { assertThatExceptionOfType(BeanNotOfRequiredTypeException.class).isThrownBy(() -> getBeanFactory().getBean("rod", BeanFactory.class)) .satisfies(ex -> { @@ -126,21 +121,19 @@ public void getInstanceByNonmatchingClass() { } @Test - public void getSharedInstanceByMatchingClass() { + protected void getSharedInstanceByMatchingClass() { Object o = getBeanFactory().getBean("rod", TestBean.class); - boolean condition = o instanceof TestBean; - assertThat(condition).as("Rod bean is a TestBean").isTrue(); + assertThat(o).isInstanceOf(TestBean.class); } @Test - public void getSharedInstanceByMatchingClassNoCatch() { + protected void getSharedInstanceByMatchingClassNoCatch() { Object o = getBeanFactory().getBean("rod", TestBean.class); - boolean condition = o instanceof TestBean; - assertThat(condition).as("Rod bean is a TestBean").isTrue(); + assertThat(o).isInstanceOf(TestBean.class); } @Test - public void getSharedInstanceByNonmatchingClass() { + protected void getSharedInstanceByNonmatchingClass() { assertThatExceptionOfType(BeanNotOfRequiredTypeException.class).isThrownBy(() -> getBeanFactory().getBean("rod", BeanFactory.class)) .satisfies(ex -> { @@ -151,18 +144,15 @@ public void getSharedInstanceByNonmatchingClass() { } @Test - public void sharedInstancesAreEqual() { + protected void sharedInstancesAreEqual() { Object o = getBeanFactory().getBean("rod"); - boolean condition1 = o instanceof TestBean; - assertThat(condition1).as("Rod bean1 is a TestBean").isTrue(); + assertThat(o).isInstanceOf(TestBean.class); Object o1 = getBeanFactory().getBean("rod"); - boolean condition = o1 instanceof TestBean; - assertThat(condition).as("Rod bean2 is a TestBean").isTrue(); - assertThat(o).as("Object equals applies").isSameAs(o1); + assertThat(o1).isInstanceOf(TestBean.class).isSameAs(o); } @Test - public void prototypeInstancesAreIndependent() { + protected void prototypeInstancesAreIndependent() { TestBean tb1 = (TestBean) getBeanFactory().getBean("kathy"); TestBean tb2 = (TestBean) getBeanFactory().getBean("kathy"); assertThat(tb1).as("ref equal DOES NOT apply").isNotSameAs(tb2); @@ -176,36 +166,37 @@ public void prototypeInstancesAreIndependent() { } @Test - public void notThere() { + protected void notThere() { assertThat(getBeanFactory().containsBean("Mr Squiggle")).isFalse(); assertThatExceptionOfType(BeansException.class).isThrownBy(() -> getBeanFactory().getBean("Mr Squiggle")); } @Test - public void validEmpty() { + protected void validEmpty() { Object o = getBeanFactory().getBean("validEmpty"); - boolean condition = o instanceof TestBean; - assertThat(condition).as("validEmpty bean is a TestBean").isTrue(); - TestBean ve = (TestBean) o; - assertThat(ve.getName() == null && ve.getAge() == 0 && ve.getSpouse() == null).as("Valid empty has defaults").isTrue(); + assertThat(o).isInstanceOfSatisfying(TestBean.class, ve -> { + assertThat(ve.getName()).isNull(); + assertThat(ve.getAge()).isEqualTo(0); + assertThat(ve.getSpouse()).isNull(); + }); } @Test - public void typeMismatch() { + protected void typeMismatch() { assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(() -> getBeanFactory().getBean("typeMismatch")) .withCauseInstanceOf(TypeMismatchException.class); } @Test - public void grandparentDefinitionFoundInBeanFactory() throws Exception { + protected void grandparentDefinitionFoundInBeanFactory() { TestBean dad = (TestBean) getBeanFactory().getBean("father"); assertThat(dad.getName().equals("Albert")).as("Dad has correct name").isTrue(); } @Test - public void factorySingleton() throws Exception { + protected void factorySingleton() { assertThat(getBeanFactory().isSingleton("&singletonFactory")).isTrue(); assertThat(getBeanFactory().isSingleton("singletonFactory")).isTrue(); TestBean tb = (TestBean) getBeanFactory().getBean("singletonFactory"); @@ -217,12 +208,11 @@ public void factorySingleton() throws Exception { } @Test - public void factoryPrototype() throws Exception { + protected void factoryPrototype() { assertThat(getBeanFactory().isSingleton("&prototypeFactory")).isTrue(); assertThat(getBeanFactory().isSingleton("prototypeFactory")).isFalse(); TestBean tb = (TestBean) getBeanFactory().getBean("prototypeFactory"); - boolean condition = !tb.getName().equals(DummyFactory.SINGLETON_NAME); - assertThat(condition).isTrue(); + assertThat(tb.getName()).isNotEqualTo(DummyFactory.SINGLETON_NAME); TestBean tb2 = (TestBean) getBeanFactory().getBean("prototypeFactory"); assertThat(tb).as("Prototype references !=").isNotSameAs(tb2); } @@ -232,7 +222,7 @@ public void factoryPrototype() throws Exception { * This is only possible if we're dealing with a factory */ @Test - public void getFactoryItself() throws Exception { + protected void getFactoryItself() { assertThat(getBeanFactory().getBean("&singletonFactory")).isNotNull(); } @@ -240,7 +230,7 @@ public void getFactoryItself() throws Exception { * Check that afterPropertiesSet gets called on factory */ @Test - public void factoryIsInitialized() throws Exception { + protected void factoryIsInitialized() { TestBean tb = (TestBean) getBeanFactory().getBean("singletonFactory"); assertThat(tb).isNotNull(); DummyFactory factory = (DummyFactory) getBeanFactory().getBean("&singletonFactory"); @@ -251,7 +241,7 @@ public void factoryIsInitialized() throws Exception { * It should be illegal to dereference a normal bean as a factory. */ @Test - public void rejectsFactoryGetOnNormalBean() { + protected void rejectsFactoryGetOnNormalBean() { assertThatExceptionOfType(BeanIsNotAFactoryException.class).isThrownBy(() -> getBeanFactory().getBean("&rod")); } @@ -259,7 +249,7 @@ public void rejectsFactoryGetOnNormalBean() { // TODO: refactor in AbstractBeanFactory (tests for AbstractBeanFactory) // and rename this class @Test - public void aliasing() { + protected void aliasing() { BeanFactory bf = getBeanFactory(); if (!(bf instanceof ConfigurableBeanFactory cbf)) { return; @@ -277,17 +267,4 @@ public void aliasing() { assertThat(rod).isSameAs(aliasRod); } - - public static class TestBeanEditor extends PropertyEditorSupport { - - @Override - public void setAsText(String text) { - TestBean tb = new TestBean(); - StringTokenizer st = new StringTokenizer(text, "_"); - tb.setName(st.nextToken()); - tb.setAge(Integer.parseInt(st.nextToken())); - setValue(tb); - } - } - } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java index c9644f5a6c63..21fa185919ee 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.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. @@ -44,7 +44,7 @@ protected ListableBeanFactory getListableBeanFactory() { * Subclasses can override this. */ @Test - public void count() { + protected void count() { assertCount(13); } @@ -66,7 +66,7 @@ protected void assertTestBeanCount(int count) { } @Test - public void getDefinitionsForNoSuchClass() { + protected void getDefinitionsForNoSuchClass() { String[] defnames = getListableBeanFactory().getBeanNamesForType(String.class); assertThat(defnames.length).as("No string definitions").isEqualTo(0); } @@ -76,7 +76,7 @@ public void getDefinitionsForNoSuchClass() { * what type factories may return, and it may even change over time.) */ @Test - public void getCountForFactoryClass() { + protected void getCountForFactoryClass() { assertThat(getListableBeanFactory().getBeanNamesForType(FactoryBean.class).length).as("Should have 2 factories, not " + getListableBeanFactory().getBeanNamesForType(FactoryBean.class).length).isEqualTo(2); @@ -85,7 +85,7 @@ public void getCountForFactoryClass() { } @Test - public void containsBeanDefinition() { + protected void containsBeanDefinition() { assertThat(getListableBeanFactory().containsBeanDefinition("rod")).isTrue(); assertThat(getListableBeanFactory().containsBeanDefinition("roderick")).isTrue(); } diff --git a/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinConfiguration.kt b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinConfiguration.kt new file mode 100644 index 000000000000..24a61d0744bc --- /dev/null +++ b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinConfiguration.kt @@ -0,0 +1,28 @@ +/* + * 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.beans.testfixture.beans + +class KotlinConfiguration { + + fun stringBean(): String { + return "Hello" + } + + suspend fun suspendingStringBean(): String { + return "Hello" + } +} diff --git a/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/factory/generator/factory/KotlinFactory.kt b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/factory/generator/factory/KotlinFactory.kt new file mode 100644 index 000000000000..408d94214699 --- /dev/null +++ b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/factory/generator/factory/KotlinFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.beans.testfixture.beans.factory.generator.factory + +class KotlinFactory { + + companion object { + + @JvmStatic + fun create() = "test" + + @JvmStatic + suspend fun suspendingCreate() = "test" + } +} diff --git a/spring-beans/src/testFixtures/resources/META-INF/spring/aot.factories b/spring-beans/src/testFixtures/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000000..5f3eadf96ddf --- /dev/null +++ b/spring-beans/src/testFixtures/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.generate.ValueCodeGenerator$Delegate=\ +org.springframework.beans.testfixture.beans.factory.aot.CustomPropertyValue$ValueCodeGeneratorDelegate \ No newline at end of file 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 f9438ad43052..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 @@ -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,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); @@ -199,7 +192,7 @@ void typeStereotypeOnIndexedInterface() { @Test void embeddedCandidatesAreDetected() - throws IOException, ClassNotFoundException { + throws ClassNotFoundException { // Validate nested type structure String nestedType = "org.springframework.context.index.sample.SampleEmbedded.Another$AnotherPublicCandidate"; Class type = ClassUtils.forName(nestedType, getClass().getClassLoader()); @@ -249,8 +242,7 @@ private CandidateComponentsMetadata readGeneratedMetadata(File outputLocation) { File metadataFile = new File(outputLocation, MetadataStore.METADATA_PATH); if (metadataFile.isFile()) { try (FileInputStream fileInputStream = new FileInputStream(metadataFile)) { - CandidateComponentsMetadata metadata = PropertiesMarshaller.read(fileInputStream); - return metadata; + return PropertiesMarshaller.read(fileInputStream); } catch (IOException ex) { throw new IllegalStateException("Failed to read metadata from disk", ex); 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-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java index e518841ef3a5..fcda73406e8d 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,10 +33,10 @@ * @author Stephane Nicoll * @author Vedran Pavic */ -public class PropertiesMarshallerTests { +class PropertiesMarshallerTests { @Test - public void readWrite() throws IOException { + void readWrite() throws IOException { CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); metadata.add(createItem("com.foo", "first", "second")); metadata.add(createItem("com.bar", "first")); @@ -51,7 +51,7 @@ public void readWrite() throws IOException { } @Test - public void metadataIsWrittenDeterministically() throws IOException { + void metadataIsWrittenDeterministically() throws IOException { CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); metadata.add(createItem("com.b", "type")); metadata.add(createItem("com.c", "type")); @@ -59,7 +59,7 @@ public void metadataIsWrittenDeterministically() throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PropertiesMarshaller.write(metadata, outputStream); - String contents = new String(outputStream.toByteArray(), StandardCharsets.ISO_8859_1); + String contents = outputStream.toString(StandardCharsets.ISO_8859_1); assertThat(contents.split(System.lineSeparator())).containsExactly("com.a=type", "com.b=type", "com.c=type"); } diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java index b96de6139374..f128d697c984 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 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,7 @@ /** * Copy of the {@code @Scope} annotation for testing purposes. */ -@Target({ ElementType.TYPE, ElementType.METHOD }) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Scope { diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java deleted file mode 100644 index fb34361664d6..000000000000 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2002-2016 the original author 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.index.sample.cdi; - -import jakarta.annotation.ManagedBean; - -/** - * Test candidate for a CDI {@link ManagedBean}. - * - * @author Stephane Nicoll - */ -@ManagedBean -public class SampleManagedBean { -} diff --git a/spring-context-support/spring-context-support.gradle b/spring-context-support/spring-context-support.gradle index 715b922b6809..851125b73e56 100644 --- a/spring-context-support/spring-context-support.gradle +++ b/spring-context-support/spring-context-support.gradle @@ -6,12 +6,12 @@ dependencies { api(project(":spring-core")) optional(project(":spring-jdbc")) // for Quartz support optional(project(":spring-tx")) // for Quartz support + optional("com.github.ben-manes.caffeine:caffeine") optional("jakarta.activation:jakarta.activation-api") optional("jakarta.mail:jakarta.mail-api") optional("javax.cache:cache-api") - optional("com.github.ben-manes.caffeine:caffeine") - optional("org.quartz-scheduler:quartz") optional("org.freemarker:freemarker") + optional("org.quartz-scheduler:quartz") testFixturesApi("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation("org.assertj:assertj-core") testFixturesImplementation("org.mockito:mockito-core") @@ -20,10 +20,11 @@ dependencies { testImplementation(testFixtures(project(":spring-context"))) testImplementation(testFixtures(project(":spring-core"))) testImplementation(testFixtures(project(":spring-tx"))) - testImplementation("org.hsqldb:hsqldb") + testImplementation("io.projectreactor:reactor-core") testImplementation("jakarta.annotation:jakarta.annotation-api") - testRuntimeOnly("org.ehcache:jcache") + testImplementation("org.hsqldb:hsqldb") + testRuntimeOnly("org.eclipse.angus:angus-mail") testRuntimeOnly("org.ehcache:ehcache") + testRuntimeOnly("org.ehcache:jcache") testRuntimeOnly("org.glassfish:jakarta.el") - testRuntimeOnly("com.sun.mail:jakarta.mail") } 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 db9dd4d92e49..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,17 +129,15 @@ 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.handle((value, ex) -> fromStoreValue(value)); + result = result.thenApply(this::toValueWrapper); } return result; } @@ -148,12 +145,18 @@ public CompletableFuture retrieve(Object key) { @SuppressWarnings("unchecked") @Override public CompletableFuture retrieve(Object key, Supplier> valueLoader) { - return (CompletableFuture) getAsyncCache().get(key, (k, e) -> valueLoader.get()); + if (isAllowNullValues()) { + return (CompletableFuture) getAsyncCache() + .get(key, (k, e) -> valueLoader.get().thenApply(this::toStoreValue)) + .thenApply(this::fromStoreValue); + } + else { + return (CompletableFuture) getAsyncCache().get(key, (k, e) -> valueLoader.get()); + } } @Override - @Nullable - protected Object lookup(Object key) { + protected @Nullable Object lookup(Object key) { if (this.cache instanceof LoadingCache loadingCache) { return loadingCache.get(key); } @@ -166,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)); @@ -198,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 44220ba86093..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 @@ -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. @@ -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; @@ -48,9 +48,10 @@ * A {@link CaffeineSpec}-compliant expression value can also be applied * via the {@link #setCacheSpecification "cacheSpecification"} bean property. * - *

    Supports the {@link Cache#retrieve(Object)} and + *

    Supports the asynchronous {@link Cache#retrieve(Object)} and * {@link Cache#retrieve(Object, Supplier)} operations through Caffeine's - * {@link AsyncCache}, when configured via {@link #setAsyncCacheMode}. + * {@link AsyncCache}, when configured via {@link #setAsyncCacheMode}, + * with early-determined cache misses. * *

    Requires Caffeine 3.0 or higher, as of Spring Framework 6.1. * @@ -69,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; @@ -193,15 +193,27 @@ public void setAsyncCacheLoader(AsyncCacheLoader cacheLoader) { * Set the common cache type that this cache manager builds to async. * This applies to {@link #setCacheNames} as well as on-demand caches. *

    Individual cache registrations (such as {@link #registerCustomCache(String, AsyncCache)} - * and {@link #registerCustomCache(String, com.github.benmanes.caffeine.cache.Cache)} + * and {@link #registerCustomCache(String, com.github.benmanes.caffeine.cache.Cache)}) * are not dependent on this setting. *

    By default, this cache manager builds regular native Caffeine caches. * To switch to async caches which can also be used through the synchronous API * but come with support for {@code Cache#retrieve}, set this flag to {@code true}. + *

    Note that while null values in the cache are tolerated in async cache mode, + * the recommendation is to disallow null values through + * {@link #setAllowNullValues setAllowNullValues(false)}. This makes the semantics + * of CompletableFuture-based access simpler and optimizes retrieval performance + * since a Caffeine-provided CompletableFuture handle does not have to get wrapped. + *

    If you come here for the adaptation of reactive types such as a Reactor + * {@code Mono} or {@code Flux} onto asynchronous caching, we recommend the standard + * arrangement for caching the produced values asynchronously in 6.1 through enabling + * this Caffeine mode. If this is not immediately possible/desirable for existing + * apps, you may set the system property "spring.cache.reactivestreams.ignore=true" + * to restore 6.0 behavior where reactive handles are treated as regular values. * @since 6.1 * @see Caffeine#buildAsync() * @see Cache#retrieve(Object) * @see Cache#retrieve(Object, Supplier) + * @see org.springframework.cache.interceptor.CacheAspectSupport#IGNORE_REACTIVESTREAMS_PROPERTY_NAME */ public void setAsyncCacheMode(boolean asyncCacheMode) { if (this.asyncCacheMode != asyncCacheMode) { @@ -238,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); @@ -290,6 +301,17 @@ public void registerCustomCache(String name, AsyncCache cache) { this.cacheMap.put(name, adaptCaffeineCache(name, cache)); } + /** + * Remove the specified cache from this cache manager, applying to + * custom caches as well as dynamically registered caches at runtime. + * @param name the name of the cache + * @since 6.1.15 + */ + public void removeCache(String name) { + this.customCacheNames.remove(name); + this.cacheMap.remove(name); + } + /** * Adapt the given new native Caffeine Cache instance to Spring's {@link Cache} * abstraction for the specified cache name. @@ -322,7 +344,7 @@ protected Cache adaptCaffeineCache(String name, AsyncCache cache * Build a common {@link CaffeineCache} instance for the specified cache name, * using the common Caffeine configuration specified on this cache manager. *

    Delegates to {@link #adaptCaffeineCache} as the adaptation method to - * Spring's cache abstraction (allowing for centralized decoration etc), + * Spring's cache abstraction (allowing for centralized decoration etc.), * passing in a freshly built native Caffeine Cache instance. * @param name the name of the cache * @return the Spring CaffeineCache adapter (or a decorator thereof) 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 e4feb09554ba..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,7 +120,7 @@ protected Collection loadCaches() { } @Override - 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 12f3d4fb7864..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. @@ -42,7 +43,7 @@ public interface JCacheConfigurer extends CachingConfigurer { /** * Return the {@link CacheResolver} bean to use to resolve exception caches for * annotation-driven cache management. Implementations must explicitly declare - * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + * {@link org.springframework.context.annotation.Bean @Bean}, for example, *
     	 * @Configuration
     	 * @EnableCaching
    @@ -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 8b20e4b14822..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 @@ -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,18 +23,17 @@ 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; /** - * Abstract implementation of {@link JCacheOperationSource} that caches attributes + * Abstract implementation of {@link JCacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. declaring method. * - *

    This implementation caches attributes by method after they are first used. - * * @author Stephane Nicoll * @author Juergen Hoeller * @since 4.1 @@ -43,24 +42,37 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOperationSource { /** - * Canonical value held in cache to indicate no caching attribute was - * found for this method and we don't need to look again. + * Canonical value held in cache to indicate no cache operation was + * found for this method, and we don't need to look again. */ - private static final Object NULL_CACHING_ATTRIBUTE = new Object(); + private static final Object NULL_CACHING_MARKER = new Object(); protected final Log logger = LogFactory.getLog(getClass()); - private final Map cache = new ConcurrentHashMap<>(1024); + private final Map operationCache = new ConcurrentHashMap<>(1024); @Override - public JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass) { + public boolean hasCacheOperation(Method method, @Nullable Class targetClass) { + return (getCacheOperation(method, targetClass, false) != null); + } + + @Override + public @Nullable JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass) { + return getCacheOperation(method, targetClass, true); + } + + private @Nullable JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass, boolean cacheNull) { + if (ReflectionUtils.isObjectMethod(method)) { + return null; + } + MethodClassKey cacheKey = new MethodClassKey(method, targetClass); - Object cached = this.cache.get(cacheKey); + Object cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? (JCacheOperation) cached : null); + return (cached != NULL_CACHING_MARKER ? (JCacheOperation) cached : null); } else { JCacheOperation operation = computeCacheOperation(method, targetClass); @@ -68,23 +80,22 @@ public JCacheOperation getCacheOperation(Method method, @Nullable Class ta if (logger.isDebugEnabled()) { logger.debug("Adding cacheable method '" + method.getName() + "' with operation: " + operation); } - this.cache.put(cacheKey, operation); + this.operationCache.put(cacheKey, operation); } - else { - this.cache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + else if (cacheNull) { + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return operation; } } - @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; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); @@ -112,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 b289b14715f4..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,6 +20,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Set; import javax.cache.annotation.CacheDefaults; import javax.cache.annotation.CacheKeyGenerator; @@ -30,9 +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.lang.Nullable; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.StringUtils; /** @@ -41,12 +44,22 @@ * {@link CacheRemoveAll} annotations. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 4.1 */ public abstract class AnnotationJCacheOperationSource extends AbstractFallbackJCacheOperationSource { + private static final Set> JCACHE_OPERATION_ANNOTATIONS = + Set.of(CacheResult.class, CachePut.class, CacheRemove.class, CacheRemoveAll.class); + + @Override - protected JCacheOperation findCacheOperation(Method method, @Nullable Class targetType) { + public boolean isCandidateClass(Class targetClass) { + return AnnotationUtils.isCandidateClass(targetClass, JCACHE_OPERATION_ANNOTATIONS); + } + + @Override + 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); @@ -75,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; @@ -162,8 +174,7 @@ protected CacheResolver getExceptionCacheResolver( } } - @Nullable - protected CacheResolverFactory determineCacheResolverFactory( + protected @Nullable CacheResolverFactory determineCacheResolverFactory( @Nullable CacheDefaults defaults, Class candidate) { if (candidate != CacheResolverFactory.class) { @@ -212,10 +223,8 @@ protected String generateDefaultCacheName(Method method) { for (Class parameterType : parameterTypes) { parameters.add(parameterType.getName()); } - - return method.getDeclaringClass().getName() - + '.' + method.getName() - + '(' + StringUtils.collectionToCommaDelimitedString(parameters) + ')'; + return method.getDeclaringClass().getName() + '.' + method.getName() + + '(' + StringUtils.collectionToCommaDelimitedString(parameters) + ')'; } private int countNonNull(Object... instances) { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java index 51fda366b04b..f6c1739340ac 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.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,15 +16,9 @@ package org.springframework.cache.jcache.interceptor; -import java.io.Serializable; -import java.lang.reflect.Method; - import org.springframework.aop.ClassFilter; import org.springframework.aop.Pointcut; import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor; -import org.springframework.aop.support.StaticMethodMatcherPointcut; -import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; /** * Advisor driven by a {@link JCacheOperationSource}, used to include a @@ -46,6 +40,7 @@ public class BeanFactoryJCacheOperationSourceAdvisor extends AbstractBeanFactory * Set the cache operation attribute source which is used to find cache * attributes. This should usually be identical to the source reference * set on the cache interceptor itself. + * @see JCacheInterceptor#setCacheOperationSource */ public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { this.pointcut.setCacheOperationSource(cacheOperationSource); @@ -64,37 +59,4 @@ public Pointcut getPointcut() { return this.pointcut; } - - private static class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { - - @Nullable - private JCacheOperationSource cacheOperationSource; - - public void setCacheOperationSource(@Nullable JCacheOperationSource cacheOperationSource) { - this.cacheOperationSource = cacheOperationSource; - } - - @Override - public boolean matches(Method method, Class targetClass) { - return (this.cacheOperationSource == null || - this.cacheOperationSource.getCacheOperation(method, targetClass) != null); - } - - @Override - public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof JCacheOperationSourcePointcut that && - ObjectUtils.nullSafeEquals(this.cacheOperationSource, that.cacheOperationSource))); - } - - @Override - public int hashCode() { - return JCacheOperationSourcePointcut.class.hashCode(); - } - - @Override - public String toString() { - return getClass().getName() + ": " + this.cacheOperationSource; - } - } - } 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 d71af3244dba..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,6 +19,8 @@ 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; @@ -39,7 +41,7 @@ public CachePutInterceptor(CacheErrorHandler errorHandler) { @Override - 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 efbfb65652e0..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -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; @@ -102,7 +102,7 @@ private static CacheParameterDetail initializeValueParameterDetail( result = parameter; } else { - throw new IllegalArgumentException("More than one @CacheValue found on " + method + ""); + throw new IllegalArgumentException("More than one @CacheValue found on " + method); } } } 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 66b583e37870..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,6 +18,8 @@ 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; @@ -38,7 +40,7 @@ protected CacheRemoveAllInterceptor(CacheErrorHandler errorHandler) { @Override - 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 84852783166a..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,6 +18,8 @@ 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; @@ -38,7 +40,7 @@ protected CacheRemoveEntryInterceptor(CacheErrorHandler errorHandler) { @Override - 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 cd3d91b37922..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * 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. @@ -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,11 +97,10 @@ 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(context.getOperation().getExceptionCacheResolver().resolveCaches(context)); + return extractFrom(exceptionCacheResolver.resolveCaches(context)); } return null; } @@ -146,9 +145,7 @@ private static CacheOperationInvoker.ThrowableWrapper rewriteCallStack( return new CacheOperationInvoker.ThrowableWrapper(clone); } - @SuppressWarnings("unchecked") - @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 4bde292d8fa5..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-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,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,6 +182,7 @@ protected T getBean(Class type) { } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected CacheManager getDefaultCacheManager() { if (getCacheManager() == null) { Assert.state(this.beanFactory != null, "BeanFactory required for default CacheManager resolution"); @@ -207,6 +202,7 @@ protected CacheManager getDefaultCacheManager() { } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected CacheResolver getDefaultCacheResolver() { if (getCacheResolver() == null) { this.cacheResolver = SingletonSupplier.of(new SimpleCacheResolver(getDefaultCacheManager())); @@ -215,6 +211,7 @@ protected CacheResolver getDefaultCacheResolver() { } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected CacheResolver getDefaultExceptionCacheResolver() { if (getExceptionCacheResolver() == null) { this.exceptionCacheResolver = SingletonSupplier.of(new LazyCacheResolver()); @@ -232,7 +229,7 @@ protected KeyGenerator getDefaultKeyGenerator() { * Only resolve the default exception cache resolver when an exception needs to be handled. *

    A non-JSR-107 setup requires either a {@link CacheManager} or a {@link CacheResolver}. * If only the latter is specified, it is not possible to extract a default exception - * {@code CacheResolver} from a custom {@code CacheResolver} implementation so we have to + * {@code CacheResolver} from a custom {@code CacheResolver} implementation, so we have to * fall back on the {@code CacheManager}. *

    This gives this weird situation of a perfectly valid configuration that breaks all * of a sudden because the JCache support is enabled. To avoid this we resolve the default 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 1dbf3c8ae6d4..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,7 +172,7 @@ public CacheOperationInvokerAdapter(CacheOperationInvoker delegate) { } @Override - 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 445a7ef82824..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,27 +18,58 @@ 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 * cache operation attributes from standard JSR-107 annotations. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 4.1 * @see org.springframework.cache.interceptor.CacheOperationSource */ public interface JCacheOperationSource { + /** + * Determine whether the given class is a candidate for cache operations + * in the metadata format of this {@code JCacheOperationSource}. + *

    If this method returns {@code false}, the methods on the given class + * will not get traversed for {@link #getCacheOperation} introspection. + * Returning {@code false} is therefore an optimization for non-affected + * classes, whereas {@code true} simply means that the class needs to get + * fully introspected for each method on the given class individually. + * @param targetClass the class to introspect + * @return {@code false} if the class is known to have no cache operation + * metadata at class or method level; {@code true} otherwise. The default + * implementation returns {@code true}, leading to regular introspection. + * @since 6.2 + * @see #hasCacheOperation + */ + default boolean isCandidateClass(Class targetClass) { + return true; + } + + /** + * Determine whether there is a JSR-107 cache operation for the given method. + * @param method the method to introspect + * @param targetClass the target class (can be {@code null}, in which case + * the declaring class of the method must be used) + * @since 6.2 + * @see #getCacheOperation + */ + default boolean hasCacheOperation(Method method, @Nullable Class targetClass) { + return (getCacheOperation(method, targetClass) != null); + } + /** * Return the cache operations for this method, or {@code null} * if the method contains no JSR-107 related metadata. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * 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 34693866eea2..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 @@ -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,40 +19,45 @@ 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.lang.Nullable; +import org.springframework.cache.CacheManager; import org.springframework.util.ObjectUtils; /** - * A Pointcut that matches if the underlying {@link JCacheOperationSource} + * A {@code Pointcut} that matches if the underlying {@link JCacheOperationSource} * has an operation for a given method. * - * @author Stephane Nicoll - * @since 4.1 - * @deprecated since 6.0.10, as it is not used by the framework anymore + * @author Juergen Hoeller + * @since 6.2 */ -@Deprecated(since = "6.0.10", forRemoval = true) @SuppressWarnings("serial") -public abstract class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { +final class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { - @Override - public boolean matches(Method method, Class targetClass) { - JCacheOperationSource cas = getCacheOperationSource(); - return (cas != null && cas.getCacheOperation(method, targetClass) != null); + private @Nullable JCacheOperationSource cacheOperationSource; + + + public JCacheOperationSourcePointcut() { + setClassFilter(new JCacheOperationSourceClassFilter()); } - /** - * Obtain the underlying {@link JCacheOperationSource} (may be {@code null}). - * To be implemented by subclasses. - */ - @Nullable - protected abstract JCacheOperationSource getCacheOperationSource(); + public void setCacheOperationSource(@Nullable JCacheOperationSource cacheOperationSource) { + this.cacheOperationSource = cacheOperationSource; + } + + @Override + public boolean matches(Method method, Class targetClass) { + return (this.cacheOperationSource == null || + this.cacheOperationSource.hasCacheOperation(method, targetClass)); + } @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof JCacheOperationSourcePointcut that && - ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); + ObjectUtils.nullSafeEquals(this.cacheOperationSource, that.cacheOperationSource))); } @Override @@ -62,7 +67,43 @@ public int hashCode() { @Override public String toString() { - return getClass().getName() + ": " + getCacheOperationSource(); + return getClass().getName() + ": " + this.cacheOperationSource; + } + + + /** + * {@link ClassFilter} that delegates to {@link JCacheOperationSource#isCandidateClass} + * for filtering classes whose methods are not worth searching to begin with. + */ + private final class JCacheOperationSourceClassFilter implements ClassFilter { + + @Override + public boolean matches(Class clazz) { + if (CacheManager.class.isAssignableFrom(clazz)) { + return false; + } + return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); + } + + private @Nullable JCacheOperationSource getCacheOperationSource() { + return cacheOperationSource; + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof JCacheOperationSourceClassFilter that && + ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); + } + + @Override + public int hashCode() { + return JCacheOperationSourceClassFilter.class.hashCode(); + } + + @Override + public String toString() { + return JCacheOperationSourceClassFilter.class.getName() + ": " + getCacheOperationSource(); + } } } 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 1aa569546131..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,6 +19,8 @@ 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; @@ -41,7 +43,7 @@ public SimpleExceptionCacheResolver(CacheManager cacheManager) { } @Override - 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 1c14c85a4b89..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,25 +78,22 @@ public Object getNativeCache() { } @Override - @Nullable - public ValueWrapper get(Object key) { + public @Nullable ValueWrapper get(Object key) { return this.targetCache.get(key); } @Override - 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); } @@ -105,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 adc888dae95f..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; @@ -484,7 +471,7 @@ protected Transport connectTransport() throws MessagingException { /** * Obtain a Transport object from the given JavaMail Session, * using the configured protocol. - *

    Can be overridden in subclasses, e.g. to return a mock Transport object. + *

    Can be overridden in subclasses, for example, to return a mock Transport object. * @see jakarta.mail.Session#getTransport(String) * @see #getSession() * @see #getProtocol() 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 84400943ced9..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 @@ -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. @@ -38,11 +38,12 @@ 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; /** * Helper class for populating a {@link jakarta.mail.internet.MimeMessage}. @@ -164,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; @@ -425,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(); } @@ -436,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; } @@ -896,7 +892,7 @@ private void setHtmlTextToMimePart(MimePart mimePart, String text) throws Messag *

    NOTE: Invoke {@code addInline} after {@link #setText}; * else, mail readers might not be able to resolve inline references correctly. * @param contentId the content ID to use. Will end up as "Content-ID" header - * in the body part, surrounded by angle brackets: e.g. "myId" → "<myId>". + * in the body part, surrounded by angle brackets: for example, "myId" → "<myId>". * Can be referenced in HTML source via src="cid:myId" expressions. * @param dataSource the {@code jakarta.activation.DataSource} to take * the content from, determining the InputStream and the content type @@ -905,12 +901,47 @@ private void setHtmlTextToMimePart(MimePart mimePart, String text) throws Messag * @see #addInline(String, org.springframework.core.io.Resource) */ public void addInline(String contentId, DataSource dataSource) throws MessagingException { + addInline(contentId, null, dataSource); + } + + /** + * Add an inline element to the MimeMessage, taking the content from a + * {@code jakarta.activation.DataSource} and assigning the provided + * {@code inlineFileName} to the element. + *

    Note that the InputStream returned by the DataSource implementation + * needs to be a fresh one on each call, as JavaMail will invoke + * {@code getInputStream()} multiple times. + *

    NOTE: Invoke {@code addInline} after {@link #setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param contentId the content ID to use. Will end up as "Content-ID" header + * in the body part, surrounded by angle brackets: for example, "myId" → "<myId>". + * Can be referenced in HTML source via src="cid:myId" expressions. + * @param inlineFilename the fileName to use for the inline element's part + * @param dataSource the {@code jakarta.activation.DataSource} to take + * the content from, determining the InputStream and the content type + * @throws MessagingException in case of errors + * @since 6.2 + * @see #addInline(String, java.io.File) + * @see #addInline(String, org.springframework.core.io.Resource) + */ + public void addInline(String contentId, @Nullable String inlineFilename, DataSource dataSource) + throws MessagingException { + Assert.notNull(contentId, "Content ID must not be null"); Assert.notNull(dataSource, "DataSource must not be null"); MimeBodyPart mimeBodyPart = new MimeBodyPart(); mimeBodyPart.setDisposition(Part.INLINE); mimeBodyPart.setContentID("<" + contentId + ">"); mimeBodyPart.setDataHandler(new DataHandler(dataSource)); + if (inlineFilename != null) { + try { + mimeBodyPart.setFileName(isEncodeFilenames() ? + MimeUtility.encodeText(inlineFilename) : inlineFilename); + } + catch (UnsupportedEncodingException ex) { + throw new MessagingException("Failed to encode inline filename", ex); + } + } getMimeMultipart().addBodyPart(mimeBodyPart); } @@ -923,7 +954,7 @@ public void addInline(String contentId, DataSource dataSource) throws MessagingE *

    NOTE: Invoke {@code addInline} after {@link #setText}; * else, mail readers might not be able to resolve inline references correctly. * @param contentId the content ID to use. Will end up as "Content-ID" header - * in the body part, surrounded by angle brackets: e.g. "myId" → "<myId>". + * in the body part, surrounded by angle brackets: for example, "myId" → "<myId>". * Can be referenced in HTML source via src="cid:myId" expressions. * @param file the File resource to take the content from * @throws MessagingException in case of errors @@ -950,7 +981,7 @@ public void addInline(String contentId, File file) throws MessagingException { *

    NOTE: Invoke {@code addInline} after {@link #setText}; * else, mail readers might not be able to resolve inline references correctly. * @param contentId the content ID to use. Will end up as "Content-ID" header - * in the body part, surrounded by angle brackets: e.g. "myId" → "<myId>". + * in the body part, surrounded by angle brackets: for example, "myId" → "<myId>". * Can be referenced in HTML source via src="cid:myId" expressions. * @param resource the resource to take the content from * @throws MessagingException in case of errors @@ -960,7 +991,8 @@ public void addInline(String contentId, File file) throws MessagingException { */ public void addInline(String contentId, Resource resource) throws MessagingException { Assert.notNull(resource, "Resource must not be null"); - String contentType = getFileTypeMap().getContentType(resource.getFilename()); + String contentType = (resource.getFilename() != null ? + getFileTypeMap().getContentType(resource.getFilename()) : MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE); addInline(contentId, resource, contentType); } @@ -976,7 +1008,7 @@ public void addInline(String contentId, Resource resource) throws MessagingExcep *

    NOTE: Invoke {@code addInline} after {@code setText}; * else, mail readers might not be able to resolve inline references correctly. * @param contentId the content ID to use. Will end up as "Content-ID" header - * in the body part, surrounded by angle brackets: e.g. "myId" → "<myId>". + * in the body part, surrounded by angle brackets: for example, "myId" → "<myId>". * Can be referenced in HTML source via src="cid:myId" expressions. * @param inputStreamSource the resource to take the content from * @param contentType the content type to use for the element @@ -989,14 +1021,75 @@ public void addInline(String contentId, Resource resource) throws MessagingExcep public void addInline(String contentId, InputStreamSource inputStreamSource, String contentType) throws MessagingException { + addInline(contentId, "inline", inputStreamSource, contentType); + } + + /** + * Add an inline element to the MimeMessage, taking the content from an + * {@code org.springframework.core.InputStreamResource}, and + * specifying the inline fileName explicitly. + *

    The content type will be determined by the name of the given + * content file. Do not use this for temporary files with arbitrary + * filenames (possibly ending in ".tmp" or the like)! + *

    Note that the InputStream returned by the InputStreamSource implementation + * needs to be a fresh one on each call, as JavaMail will invoke + * {@code getInputStream()} multiple times. + *

    NOTE: Invoke {@code addInline} after {@code setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param contentId the content ID to use. Will end up as "Content-ID" header + * in the body part, surrounded by angle brackets: for example, "myId" → "<myId>". + * Can be referenced in HTML source via src="cid:myId" expressions. + * @param inlineFilename the file name to use for the inline element + * @param inputStreamSource the resource to take the content from + * @throws MessagingException in case of errors + * @since 6.2 + * @see #setText(String) + * @see #getFileTypeMap + * @see #addInline(String, org.springframework.core.io.Resource) + * @see #addInline(String, String, jakarta.activation.DataSource) + */ + public void addInline(String contentId, String inlineFilename, InputStreamSource inputStreamSource) + throws MessagingException { + + String contentType = getFileTypeMap().getContentType(inlineFilename); + addInline(contentId, inlineFilename, inputStreamSource, contentType); + } + + /** + * Add an inline element to the MimeMessage, taking the content from an + * {@code org.springframework.core.InputStreamResource}, and + * specifying the inline fileName and content type explicitly. + *

    You can determine the content type for any given filename via a Java + * Activation Framework's FileTypeMap, for example the one held by this helper. + *

    Note that the InputStream returned by the InputStreamSource implementation + * needs to be a fresh one on each call, as JavaMail will invoke + * {@code getInputStream()} multiple times. + *

    NOTE: Invoke {@code addInline} after {@code setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param contentId the content ID to use. Will end up as "Content-ID" header + * in the body part, surrounded by angle brackets: for example, "myId" → "<myId>". + * Can be referenced in HTML source via src="cid:myId" expressions. + * @param inlineFilename the fileName to use for the inline element's part + * @param inputStreamSource the resource to take the content from + * @param contentType the content type to use for the element + * @throws MessagingException in case of errors + * @since 6.2 + * @see #setText + * @see #getFileTypeMap + * @see #addInline(String, org.springframework.core.io.Resource) + * @see #addInline(String, String, jakarta.activation.DataSource) + */ + public void addInline(String contentId, String inlineFilename, InputStreamSource inputStreamSource, String contentType) + throws MessagingException { + Assert.notNull(inputStreamSource, "InputStreamSource must not be null"); if (inputStreamSource instanceof Resource resource && resource.isOpen()) { throw new IllegalArgumentException( "Passed-in Resource contains an open stream: invalid argument. " + "JavaMail requires an InputStreamSource that creates a fresh stream for every call."); } - DataSource dataSource = createDataSource(inputStreamSource, contentType, "inline"); - addInline(contentId, dataSource); + DataSource dataSource = createDataSource(inputStreamSource, contentType, inlineFilename); + addInline(contentId, inlineFilename, dataSource); } /** 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 f6042bee9347..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 @@ -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. @@ -19,9 +19,11 @@ import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.SQLException; +import java.util.Locale; 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; @@ -33,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 @@ -85,11 +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") // Dataflow analysis limitation public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler) throws SchedulerConfigException { // Absolutely needs thread-bound DataSource to initialize. this.dataSource = SchedulerFactoryBean.getConfigTimeDataSource(); @@ -155,7 +156,7 @@ public void initialize() { String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource, DatabaseMetaData::getDatabaseProductName); productName = JdbcUtils.commonDatabaseName(productName); - if (productName != null && productName.toLowerCase().contains("hsql")) { + if (productName != null && productName.toLowerCase(Locale.ROOT).contains("hsql")) { setUseDBLocks(false); setLockHandler(new SimpleSemaphore()); } 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 cace9ab82e4b..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,7 +193,7 @@ protected void postProcessJobDetail(JobDetail jobDetail) { * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. */ @Override - 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'"); @@ -212,7 +206,7 @@ public Class getTargetClass() { * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. */ @Override - 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'"); @@ -223,8 +217,7 @@ public Object getTargetObject() { @Override - @Nullable - public JobDetail getObject() { + public @Nullable JobDetail getObject() { return this.jobDetail; } @@ -247,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 f9d4c72ab306..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,6 +194,7 @@ public void setResourceLoader(ResourceLoader resourceLoader) { /** * Register jobs and triggers (within a transaction, if possible). */ + @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 ea136c6e4fb1..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,21 +40,18 @@ */ 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; /** * Specify the Quartz {@link Scheduler} to operate on via its scheduler name in the Spring * application context or also in the Quartz {@link org.quartz.impl.SchedulerRepository}. *

    Schedulers can be registered in the repository through custom bootstrapping, - * e.g. via the {@link org.quartz.impl.StdSchedulerFactory} or + * for example, via the {@link org.quartz.impl.StdSchedulerFactory} or * {@link org.quartz.impl.DirectSchedulerFactory} factory classes. * However, in general, it's preferable to use Spring's {@link SchedulerFactoryBean} * which includes the job/trigger/listener capabilities of this accessor as well. 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 82015a57549f..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 @@ -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,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; /** @@ -319,7 +302,7 @@ public void setTaskExecutor(Executor taskExecutor) { * It is therefore strongly recommended to perform all operations on * the Scheduler within Spring-managed (or plain JTA) transactions. * Else, database locking will not properly work and might even break - * (e.g. if trying to obtain a lock on Oracle without a transaction). + * (for example, if trying to obtain a lock on Oracle without a transaction). *

    Supports both transactional and non-transactional DataSource access. * With a non-XA DataSource and local Spring transactions, a single DataSource * argument is sufficient. In case of an XA DataSource and global JTA transactions, @@ -661,6 +644,7 @@ private Scheduler prepareScheduler(SchedulerFactory schedulerFactory) throws Sch * @see #afterPropertiesSet * @see org.quartz.SchedulerFactory#getScheduler */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected Scheduler createScheduler(SchedulerFactory schedulerFactory, @Nullable String schedulerName) throws SchedulerException { @@ -736,27 +720,24 @@ protected void startScheduler(final Scheduler scheduler, final int startupDelay) } // Not using the Quartz startDelayed method since we explicitly want a daemon // thread here, not keeping the JVM alive in case of all other threads ending. - Thread schedulerThread = new Thread() { - @Override - public void run() { - try { - TimeUnit.SECONDS.sleep(startupDelay); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - // simply proceed - } - if (logger.isInfoEnabled()) { - logger.info("Starting Quartz Scheduler now, after delay of " + startupDelay + " seconds"); - } - try { - scheduler.start(); - } - catch (SchedulerException ex) { - throw new SchedulingException("Could not start Quartz Scheduler after delay", ex); - } + Thread schedulerThread = new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(startupDelay); } - }; + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + // simply proceed + } + if (logger.isInfoEnabled()) { + logger.info("Starting Quartz Scheduler now, after delay of " + startupDelay + " seconds"); + } + try { + scheduler.start(); + } + catch (SchedulerException ex) { + throw new SchedulingException("Could not start Quartz Scheduler after delay", ex); + } + }); schedulerThread.setName("Quartz Scheduler [" + scheduler.getSchedulerName() + "]"); schedulerThread.setDaemon(true); schedulerThread.start(); @@ -775,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 c2d727a45588..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 @@ -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,6 +16,8 @@ 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; @@ -26,7 +28,7 @@ /** * {@link RuntimeHintsRegistrar} implementation that makes sure {@link SchedulerFactoryBean} - * reflection entries are registered. + * reflection hints are registered. * * @author Sebastien Deleuze * @author Stephane Nicoll @@ -36,11 +38,11 @@ class SchedulerFactoryBeanRuntimeHints implements RuntimeHintsRegistrar { private static final String SCHEDULER_FACTORY_CLASS_NAME = "org.quartz.impl.StdSchedulerFactory"; - private final ReflectiveRuntimeHintsRegistrar reflectiveRegistrar = new ReflectiveRuntimeHintsRegistrar(); + private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { if (!ClassUtils.isPresent(SCHEDULER_FACTORY_CLASS_NAME, classLoader)) { return; } @@ -48,7 +50,7 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { .registerType(TypeReference.of(SCHEDULER_FACTORY_CLASS_NAME), this::typeHint) .registerTypes(TypeReference.listOf(ResourceLoaderClassLoadHelper.class, LocalTaskExecutorThreadPool.class, LocalDataSourceJobStore.class), this::typeHint); - this.reflectiveRegistrar.registerRuntimeHints(hints, LocalTaskExecutorThreadPool.class); + registrar.registerRuntimeHints(hints, LocalTaskExecutorThreadPool.class); } private void typeHint(Builder typeHint) { 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 811d76c9ef38..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 @@ -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. @@ -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 @@ -49,7 +47,7 @@ */ @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 bc3ddc27e01b..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 @@ -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.File; import java.io.IOException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -32,18 +33,20 @@ 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; /** - * Factory that configures a FreeMarker Configuration. Can be used standalone, but - * typically you will either use FreeMarkerConfigurationFactoryBean for preparing a - * Configuration as bean reference, or FreeMarkerConfigurer for web views. + * Factory that configures a FreeMarker {@link Configuration}. + * + *

    Can be used standalone, but typically you will either use + * {@link FreeMarkerConfigurationFactoryBean} for preparing a {@code Configuration} + * as a bean reference, or {@code FreeMarkerConfigurer} for web views. * *

    The optional "configLocation" property sets the location of a FreeMarker * properties file, within the current application. FreeMarker properties can be @@ -52,17 +55,18 @@ * subject to constraints set by FreeMarker. * *

    The "freemarkerVariables" property can be used to specify a Map of - * shared variables that will be applied to the Configuration via the + * shared variables that will be applied to the {@code Configuration} via the * {@code setAllSharedVariables()} method. Like {@code setSettings()}, * these entries are subject to FreeMarker constraints. * *

    The simplest way to use this class is to specify a "templateLoaderPath"; * FreeMarker does not need any further configuration then. * - *

    Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + *

    Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Darren Davison * @author Juergen Hoeller + * @author Sam Brannen * @since 03.03.2004 * @see #setConfigLocation * @see #setFreemarkerSettings @@ -77,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(); @@ -107,7 +104,7 @@ public class FreeMarkerConfigurationFactory { /** * Set the location of the FreeMarker config file. - * Alternatively, you can specify all setting locally. + *

    Alternatively, you can specify all settings locally. * @see #setFreemarkerSettings * @see #setTemplateLoaderPath */ @@ -134,25 +131,49 @@ public void setFreemarkerVariables(Map variables) { } /** - * Set the default encoding for the FreeMarker configuration. - * If not specified, FreeMarker will use the platform file encoding. - *

    Used for template rendering unless there is an explicit encoding specified - * for the rendering process (for example, on Spring's FreeMarkerView). + * Set the default encoding for the FreeMarker {@link Configuration}, which + * is used to decode byte sequences to character sequences when reading template + * files. + *

    If not specified, FreeMarker will read template files using the platform + * file encoding (defined by the JVM system property {@code file.encoding}) + * or UTF-8 if the platform file encoding is undefined. + *

    Note that the supplied encoding may or may not be used for template + * rendering. See the documentation for Spring's {@code FreeMarkerView} and + * {@code FreeMarkerViewResolver} implementations for further details. + * @see #setDefaultCharset(Charset) * @see freemarker.template.Configuration#setDefaultEncoding * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setEncoding + * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setContentType + * @see org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver#setContentType + * @see org.springframework.web.reactive.result.view.freemarker.FreeMarkerView#setEncoding + * @see org.springframework.web.reactive.result.view.freemarker.FreeMarkerView#setSupportedMediaTypes + * @see org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver#setSupportedMediaTypes */ public void setDefaultEncoding(String defaultEncoding) { this.defaultEncoding = defaultEncoding; } /** - * Set a List of {@code TemplateLoader}s that will be used to search - * for templates. For example, one or more custom loaders such as database - * loaders could be configured and injected here. - *

    The {@link TemplateLoader TemplateLoaders} specified here will be - * registered before the default template loaders that this factory - * registers (such as loaders for specified "templateLoaderPaths" or any - * loaders registered in {@link #postProcessTemplateLoaders}). + * Set the {@link Charset} for the default encoding for the FreeMarker + * {@link Configuration}, which is used to decode byte sequences to character + * sequences when reading template files. + *

    See {@link #setDefaultEncoding(String)} for details. + * @since 6.2 + * @see java.nio.charset.StandardCharsets + */ + public void setDefaultCharset(Charset defaultCharset) { + this.defaultEncoding = defaultCharset.name(); + } + + /** + * Set a list of {@link TemplateLoader TemplateLoaders} that will be used to + * search for templates. + *

    For example, one or more custom loaders such as database loaders could + * be configured and injected here. + *

    The {@code TemplateLoaders} specified here will be registered before + * the default template loaders that this factory registers (such as loaders + * for specified "templateLoaderPaths" or any loaders registered in + * {@link #postProcessTemplateLoaders}). * @see #setTemplateLoaderPaths * @see #postProcessTemplateLoaders */ @@ -161,13 +182,14 @@ public void setPreTemplateLoaders(TemplateLoader... preTemplateLoaders) { } /** - * Set a List of {@code TemplateLoader}s that will be used to search - * for templates. For example, one or more custom loaders such as database - * loaders can be configured. - *

    The {@link TemplateLoader TemplateLoaders} specified here will be - * registered after the default template loaders that this factory - * registers (such as loaders for specified "templateLoaderPaths" or any - * loaders registered in {@link #postProcessTemplateLoaders}). + * Set a list of {@link TemplateLoader TemplateLoaders} that will be used to + * search for templates. + *

    For example, one or more custom loaders such as database loaders could + * be configured and injected here. + *

    The {@code TemplateLoaders} specified here will be registered after + * the default template loaders that this factory registers (such as loaders + * for specified "templateLoaderPaths" or any loaders registered in + * {@link #postProcessTemplateLoaders}). * @see #setTemplateLoaderPaths * @see #postProcessTemplateLoaders */ @@ -177,7 +199,7 @@ public void setPostTemplateLoaders(TemplateLoader... postTemplateLoaders) { /** * Set the Freemarker template loader path via a Spring resource location. - * See the "templateLoaderPaths" property for details on path handling. + *

    See the "templateLoaderPaths" property for details on path handling. * @see #setTemplateLoaderPaths */ public void setTemplateLoaderPath(String templateLoaderPath) { @@ -188,28 +210,29 @@ public void setTemplateLoaderPath(String templateLoaderPath) { * Set multiple Freemarker template loader paths via Spring resource locations. *

    When populated via a String, standard URLs like "file:" and "classpath:" * pseudo URLs are supported, as understood by ResourceEditor. Allows for - * relative paths when running in an ApplicationContext. - *

    Will define a path for the default FreeMarker template loader. - * If a specified resource cannot be resolved to a {@code java.io.File}, - * a generic SpringTemplateLoader will be used, without modification detection. - *

    To enforce the use of SpringTemplateLoader, i.e. to not resolve a path - * as file system resource in any case, turn off the "preferFileSystemAccess" + * relative paths when running in an {@code ApplicationContext}. + *

    Will define a path for the default FreeMarker template loader. If a + * specified resource cannot be resolved to a {@code java.io.File}, a generic + * {@link SpringTemplateLoader} will be used, without modification detection. + *

    To enforce the use of {@code SpringTemplateLoader}, i.e. to not resolve + * a path as file system resource in any case, turn off the "preferFileSystemAccess" * flag. See the latter's javadoc for details. *

    If you wish to specify your own list of TemplateLoaders, do not set this - * property and instead use {@code setTemplateLoaders(List templateLoaders)} + * property and instead use {@link #setPostTemplateLoaders(TemplateLoader...)}. * @see org.springframework.core.io.ResourceEditor * @see org.springframework.context.ApplicationContext#getResource * @see freemarker.template.Configuration#setDirectoryForTemplateLoading * @see SpringTemplateLoader + * @see #setPreferFileSystemAccess(boolean) */ public void setTemplateLoaderPaths(String... templateLoaderPaths) { this.templateLoaderPaths = templateLoaderPaths; } /** - * Set the Spring ResourceLoader to use for loading FreeMarker template files. - * The default is DefaultResourceLoader. Will get overridden by the - * ApplicationContext if running in a context. + * Set the {@link ResourceLoader} to use for loading FreeMarker template files. + *

    The default is {@link DefaultResourceLoader}. Will get overridden by the + * {@code ApplicationContext} if running in a context. * @see org.springframework.core.io.DefaultResourceLoader */ public void setResourceLoader(ResourceLoader resourceLoader) { @@ -217,7 +240,7 @@ public void setResourceLoader(ResourceLoader resourceLoader) { } /** - * Return the Spring ResourceLoader to use for loading FreeMarker template files. + * Return the {@link ResourceLoader} to use for loading FreeMarker template files. */ protected ResourceLoader getResourceLoader() { return this.resourceLoader; @@ -225,11 +248,11 @@ protected ResourceLoader getResourceLoader() { /** * Set whether to prefer file system access for template loading. - * File system access enables hot detection of template changes. + *

    File system access enables hot detection of template changes. *

    If this is enabled, FreeMarkerConfigurationFactory will try to resolve * the specified "templateLoaderPath" as file system resource (which will work * for expanded class path resources and ServletContext resources too). - *

    Default is "true". Turn this off to always load via SpringTemplateLoader + *

    Default is "true". Turn this off to always load via {@link SpringTemplateLoader} * (i.e. as stream, without hot detection of template changes), which might * be necessary if some of your templates reside in an expanded classes * directory while others reside in jar files. @@ -248,8 +271,8 @@ protected boolean isPreferFileSystemAccess() { /** - * Prepare the FreeMarker Configuration and return it. - * @return the FreeMarker Configuration object + * Prepare the FreeMarker {@link Configuration} and return it. + * @return the FreeMarker {@code Configuration} object * @throws IOException if the config file wasn't found * @throws TemplateException on FreeMarker initialization failure */ @@ -314,11 +337,12 @@ public Configuration createConfiguration() throws IOException, TemplateException } /** - * Return a new Configuration object. Subclasses can override this for custom - * initialization (e.g. specifying a FreeMarker compatibility level which is a - * new feature in FreeMarker 2.3.21), or for using a mock object for testing. - *

    Called by {@code createConfiguration()}. - * @return the Configuration object + * Return a new {@link Configuration} object. + *

    Subclasses can override this for custom initialization — for example, + * to specify a FreeMarker compatibility level (which is a new feature in + * FreeMarker 2.3.21), or to use a mock object for testing. + *

    Called by {@link #createConfiguration()}. + * @return the {@code Configuration} object * @throws IOException if a config file wasn't found * @throws TemplateException on FreeMarker initialization failure * @see #createConfiguration() @@ -328,11 +352,11 @@ protected Configuration newConfiguration() throws IOException, TemplateException } /** - * Determine a FreeMarker TemplateLoader for the given path. - *

    Default implementation creates either a FileTemplateLoader or - * a SpringTemplateLoader. + * Determine a FreeMarker {@link TemplateLoader} for the given path. + *

    Default implementation creates either a {@link FileTemplateLoader} or + * a {@link SpringTemplateLoader}. * @param templateLoaderPath the path to load templates from - * @return an appropriate TemplateLoader + * @return an appropriate {@code TemplateLoader} * @see freemarker.cache.FileTemplateLoader * @see SpringTemplateLoader */ @@ -366,9 +390,9 @@ protected TemplateLoader getTemplateLoaderForPath(String templateLoaderPath) { /** * To be overridden by subclasses that want to register custom - * TemplateLoader instances after this factory created its default + * {@link TemplateLoader} instances after this factory created its default * template loaders. - *

    Called by {@code createConfiguration()}. Note that specified + *

    Called by {@link #createConfiguration()}. Note that specified * "postTemplateLoaders" will be registered after any loaders * registered by this callback; as a consequence, they are not * included in the given List. @@ -381,14 +405,13 @@ protected void postProcessTemplateLoaders(List templateLoaders) } /** - * Return a TemplateLoader based on the given TemplateLoader list. - * If more than one TemplateLoader has been registered, a FreeMarker - * MultiTemplateLoader needs to be created. - * @param templateLoaders the final List of TemplateLoader instances + * Return a {@link TemplateLoader} based on the given {@code TemplateLoader} list. + *

    If more than one TemplateLoader has been registered, a FreeMarker + * {@link MultiTemplateLoader} will be created. + * @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"); @@ -404,10 +427,10 @@ protected TemplateLoader getAggregateTemplateLoader(List templat /** * To be overridden by subclasses that want to perform custom - * post-processing of the Configuration object after this factory + * post-processing of the {@link Configuration} object after this factory * performed its default initialization. - *

    Called by {@code createConfiguration()}. - * @param config the current Configuration object + *

    Called by {@link #createConfiguration()}. + * @param config the current {@code Configuration} object * @throws IOException if a config file wasn't found * @throws TemplateException on FreeMarker initialization failure * @see #createConfiguration() 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 5639ac06a91f..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 @@ -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. @@ -20,29 +20,32 @@ 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 Configuration and provides it as - * bean reference. This bean is intended for any kind of usage of FreeMarker - * in application code, e.g. for generating email content. For web views, - * FreeMarkerConfigurer is used to set up a FreeMarkerConfigurationFactory. + * Factory bean that creates a FreeMarker {@link Configuration} and provides it + * as a bean reference. * - * The simplest way to use this class is to specify just a "templateLoaderPath"; + *

    This bean is intended for any kind of usage of FreeMarker in application + * code — for example, for generating email content. For web views, + * {@code FreeMarkerConfigurer} is used to set up a {@link FreeMarkerConfigurationFactory}. + * + *

    The simplest way to use this class is to specify just a "templateLoaderPath"; * you do not need any further configuration then. For example, in a web * application context: * *

     <bean id="freemarkerConfiguration" class="org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean">
      *   <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
      * </bean>
    - - * See the base class FreeMarkerConfigurationFactory for configuration details. * - *

    Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + *

    See the {@link FreeMarkerConfigurationFactory} base class for configuration + * details. + * + *

    Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Darren Davison * @since 03.03.2004 @@ -54,8 +57,7 @@ public class FreeMarkerConfigurationFactoryBean extends FreeMarkerConfigurationFactory implements FactoryBean, InitializingBean, ResourceLoaderAware { - @Nullable - private Configuration configuration; + private @Nullable Configuration configuration; @Override @@ -65,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/FreeMarkerTemplateUtils.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java index e1ca06bf99e3..022ebd9bdec3 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.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. @@ -24,7 +24,8 @@ /** * Utility class for working with FreeMarker. - * Provides convenience methods to process a FreeMarker template with a model. + * + *

    Provides convenience methods to process a FreeMarker template with a model. * * @author Juergen Hoeller * @since 14.03.2004 @@ -33,12 +34,12 @@ public abstract class FreeMarkerTemplateUtils { /** * Process the specified FreeMarker template with the given model and write - * the result to the given Writer. - *

    When using this method to prepare a text for a mail to be sent with Spring's + * the result to a String. + *

    When using this method to prepare text for a mail to be sent with Spring's * mail support, consider wrapping IO/TemplateException in MailPreparationException. * @param model the model object, typically a Map that contains model names * as keys and model objects as values - * @return the result as String + * @return the result as a String * @throws IOException if the template wasn't found or couldn't be read * @throws freemarker.template.TemplateException if rendering failed * @see org.springframework.mail.MailPreparationException 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 d4140aa681f1..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,15 +23,17 @@ 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 via a Spring {@link ResourceLoader}. - * Used by {@link FreeMarkerConfigurationFactory} for any resource loader path that cannot - * be resolved to a {@link java.io.File}. + * FreeMarker {@link TemplateLoader} adapter that loads template files via a + * Spring {@link ResourceLoader}. + * + *

    Used by {@link FreeMarkerConfigurationFactory} for any resource loader path + * that cannot be resolved to a {@link java.io.File}. * * @author Juergen Hoeller * @since 14.03.2004 @@ -48,7 +50,7 @@ public class SpringTemplateLoader implements TemplateLoader { /** - * Create a new SpringTemplateLoader. + * Create a new {@code SpringTemplateLoader}. * @param resourceLoader the Spring ResourceLoader to use * @param templateLoaderPath the template loader path to use */ @@ -66,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/main/resources/org/springframework/mail/javamail/mime.types b/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types index f325ad37d8b5..7ce3bd4ab30b 100644 --- a/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types +++ b/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types @@ -262,7 +262,7 @@ application/astound asn # SPECIAL EMBEDDED OBJECT -# OLE script e.g. Visual Basic (Ncompass) +# OLE script, for example, Visual Basic (Ncompass) application/x-olescript axs # OLE Object (Microsoft/NCompass) application/x-oleobject ods diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java index 261ab391b1a9..c88b3c8d2b24 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.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. @@ -25,6 +25,7 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; +import org.springframework.cache.support.SimpleValueWrapper; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -170,9 +171,9 @@ void asyncMode() { assertThat(cache1.get("key3", () -> (String) null)).isNull(); assertThat(cache1.get("key3", () -> (String) null)).isNull(); - assertThat(cache1.retrieve("key1").join()).isEqualTo("value1"); - assertThat(cache1.retrieve("key2").join()).isEqualTo(2); - assertThat(cache1.retrieve("key3").join()).isNull(); + assertThat(cache1.retrieve("key1").join()).isEqualTo(new SimpleValueWrapper("value1")); + assertThat(cache1.retrieve("key2").join()).isEqualTo(new SimpleValueWrapper(2)); + assertThat(cache1.retrieve("key3").join()).isEqualTo(new SimpleValueWrapper(null)); cache1.evict("key3"); assertThat(cache1.retrieve("key3")).isNull(); assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join()) @@ -180,10 +181,50 @@ void asyncMode() { assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join()) .isEqualTo("value3"); cache1.evict("key3"); + assertThat(cache1.retrieve("key3")).isNull(); assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture(null)).join()).isNull(); + assertThat(cache1.retrieve("key3").join()).isEqualTo(new SimpleValueWrapper(null)); assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture(null)).join()).isNull(); } + @Test + void asyncModeWithoutNullValues() { + CaffeineCacheManager cm = new CaffeineCacheManager(); + cm.setAsyncCacheMode(true); + cm.setAllowNullValues(false); + + Cache cache1 = cm.getCache("c1"); + assertThat(cache1).isInstanceOf(CaffeineCache.class); + Cache cache1again = cm.getCache("c1"); + assertThat(cache1).isSameAs(cache1again); + Cache cache2 = cm.getCache("c2"); + assertThat(cache2).isInstanceOf(CaffeineCache.class); + Cache cache2again = cm.getCache("c2"); + assertThat(cache2).isSameAs(cache2again); + Cache cache3 = cm.getCache("c3"); + assertThat(cache3).isInstanceOf(CaffeineCache.class); + Cache cache3again = cm.getCache("c3"); + assertThat(cache3).isSameAs(cache3again); + + cache1.put("key1", "value1"); + assertThat(cache1.get("key1").get()).isEqualTo("value1"); + cache1.put("key2", 2); + assertThat(cache1.get("key2").get()).isEqualTo(2); + cache1.evict("key3"); + assertThat(cache1.get("key3")).isNull(); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + cache1.evict("key3"); + + assertThat(cache1.retrieve("key1").join()).isEqualTo("value1"); + assertThat(cache1.retrieve("key2").join()).isEqualTo(2); + assertThat(cache1.retrieve("key3")).isNull(); + assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join()) + .isEqualTo("value3"); + assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join()) + .isEqualTo("value3"); + } + @Test void changeCaffeineRecreateCache() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); @@ -224,7 +265,6 @@ void changeCacheLoaderRecreateCache() { CaffeineCacheManager cm = new CaffeineCacheManager("c1"); Cache cache1 = cm.getCache("c1"); - @SuppressWarnings("unchecked") CacheLoader loader = mock(); cm.setCacheLoader(loader); diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineReactiveCachingTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineReactiveCachingTests.java new file mode 100644 index 000000000000..f92e69c2f628 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineReactiveCachingTests.java @@ -0,0 +1,184 @@ +/* + * 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.cache.caffeine; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for annotation-based caching methods that use reactive operators. + * + * @author Juergen Hoeller + * @since 6.1 + */ +class CaffeineReactiveCachingTests { + + @ParameterizedTest + @ValueSource(classes = {AsyncCacheModeConfig.class, AsyncCacheModeConfig.class}) + void cacheHitDetermination(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(); + Long r2 = service.cacheFuture(key).join(); + Long r3 = service.cacheFuture(key).join(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + key = new Object(); + + r1 = service.cacheMono(key).block(); + r2 = service.cacheMono(key).block(); + r3 = service.cacheMono(key).block(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + key = new Object(); + + r1 = service.cacheFlux(key).blockFirst(); + r2 = service.cacheFlux(key).blockFirst(); + r3 = service.cacheFlux(key).blockFirst(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + key = new Object(); + + List l1 = service.cacheFlux(key).collectList().block(); + List l2 = service.cacheFlux(key).collectList().block(); + List l3 = service.cacheFlux(key).collectList().block(); + + assertThat(l1).isNotNull(); + assertThat(l1).isEqualTo(l2).isEqualTo(l3); + + key = new Object(); + + r1 = service.cacheMono(key).block(); + r2 = service.cacheMono(key).block(); + r3 = service.cacheMono(key).block(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + // Same key as for Mono, reusing its cached value + + r1 = service.cacheFlux(key).blockFirst(); + r2 = service.cacheFlux(key).blockFirst(); + r3 = service.cacheFlux(key).blockFirst(); + + assertThat(r1).isNotNull(); + assertThat(r1).isSameAs(r2).isSameAs(r3); + + ctx.close(); + } + + + @ParameterizedTest + @ValueSource(classes = {AsyncCacheModeConfig.class, AsyncCacheModeConfig.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 { + + private final AtomicLong counter = new AtomicLong(); + + @Cacheable + CompletableFuture cacheFuture(Object arg) { + return CompletableFuture.completedFuture(this.counter.getAndIncrement()); + } + + @Cacheable + Mono cacheMono(Object arg) { + // here counter not only reflects invocations of cacheMono but subscriptions to + // the returned Mono as well. See https://github.com/spring-projects/spring-framework/issues/32370 + return Mono.defer(() -> Mono.just(this.counter.getAndIncrement())); + } + + @Cacheable + Flux cacheFlux(Object arg) { + // here counter not only reflects invocations of cacheFlux but subscriptions to + // the returned Flux as well. See https://github.com/spring-projects/spring-framework/issues/32370 + return Flux.defer(() -> Flux.just(this.counter.getAndIncrement(), 0L, -1L, -2L, -3L)); + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class AsyncCacheModeConfig { + + @Bean + CacheManager cacheManager() { + CaffeineCacheManager cm = new CaffeineCacheManager("first"); + cm.setAsyncCacheMode(true); + return cm; + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class AsyncCacheModeWithoutNullValuesConfig { + + @Bean + CacheManager cacheManager() { + CaffeineCacheManager ccm = new CaffeineCacheManager("first"); + ccm.setAsyncCacheMode(true); + ccm.setAllowNullValues(false); + return ccm; + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java index 502b17f6dcdf..481bd9b912f5 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.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. @@ -32,7 +32,7 @@ /** * @author Stephane Nicoll */ -public class JCacheCacheManagerTests extends AbstractTransactionSupportingCacheManagerTests { +class JCacheCacheManagerTests extends AbstractTransactionSupportingCacheManagerTests { private CacheManagerMock cacheManagerMock; @@ -42,7 +42,7 @@ public class JCacheCacheManagerTests extends AbstractTransactionSupportingCacheM @BeforeEach - public void setupOnce() { + void setupOnce() { cacheManagerMock = new CacheManagerMock(); cacheManagerMock.addCache(CACHE_NAME); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java index e77e948c802b..61f2020d7111 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.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. @@ -46,7 +46,7 @@ * @author Stephane Nicoll * @author Juergen Hoeller */ -public class JCacheEhCacheAnnotationTests extends AbstractCacheAnnotationTests { +class JCacheEhCacheAnnotationTests extends AbstractCacheAnnotationTests { private final TransactionTemplate txTemplate = new TransactionTemplate(new CallCountingTransactionManager()); @@ -68,7 +68,7 @@ protected CachingProvider getCachingProvider() { } @AfterEach - public void shutdown() { + void shutdown() { if (jCacheManager != null) { jCacheManager.close(); } @@ -82,22 +82,22 @@ public void testCustomCacheManager() { } @Test - public void testEvictWithTransaction() { + void testEvictWithTransaction() { txTemplate.executeWithoutResult(s -> testEvict(this.cs, false)); } @Test - public void testEvictEarlyWithTransaction() { + void testEvictEarlyWithTransaction() { txTemplate.executeWithoutResult(s -> testEvictEarly(this.cs)); } @Test - public void testEvictAllWithTransaction() { + void testEvictAllWithTransaction() { txTemplate.executeWithoutResult(s -> testEvictAll(this.cs, false)); } @Test - public void testEvictAllEarlyWithTransaction() { + void testEvictAllEarlyWithTransaction() { txTemplate.executeWithoutResult(s -> testEvictAllEarly(this.cs)); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java index 289bb82008aa..1adea115c396 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.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,7 +33,7 @@ /** * @author Stephane Nicoll */ -public class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests { +class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests { private CacheManager cacheManager; @@ -45,7 +45,7 @@ public class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests()); this.cacheManager.createCache(CACHE_NAME_NO_NULL, new MutableConfiguration<>()); @@ -61,7 +61,7 @@ protected CachingProvider getCachingProvider() { } @AfterEach - public void shutdown() { + void shutdown() { if (this.cacheManager != null) { this.cacheManager.close(); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java index 62f94db25fcd..8cc2254b1554 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.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. @@ -24,6 +24,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; @@ -43,9 +46,11 @@ import static org.assertj.core.api.Assertions.assertThatRuntimeException; /** + * Tests that use a custom {@link JCacheInterceptor}. + * * @author Stephane Nicoll */ -public class JCacheCustomInterceptorTests { +class JCacheCustomInterceptorTests { protected ConfigurableApplicationContext ctx; @@ -55,22 +60,25 @@ public class JCacheCustomInterceptorTests { @BeforeEach - public void setup() { - ctx = new AnnotationConfigApplicationContext(EnableCachingConfig.class); + void setup() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getBeanFactory().addBeanPostProcessor( + new CacheInterceptorBeanPostProcessor(context.getBeanFactory())); + context.register(EnableCachingConfig.class); + context.refresh(); + this.ctx = context; cs = ctx.getBean("service", JCacheableService.class); exceptionCache = ctx.getBean("exceptionCache", Cache.class); } @AfterEach - public void tearDown() { - if (ctx != null) { - ctx.close(); - } + void tearDown() { + ctx.close(); } @Test - public void onlyOneInterceptorIsAvailable() { + void onlyOneInterceptorIsAvailable() { Map interceptors = ctx.getBeansOfType(JCacheInterceptor.class); assertThat(interceptors).as("Only one interceptor should be defined").hasSize(1); JCacheInterceptor interceptor = interceptors.values().iterator().next(); @@ -78,17 +86,17 @@ public void onlyOneInterceptorIsAvailable() { } @Test - public void customInterceptorAppliesWithRuntimeException() { + void customInterceptorAppliesWithRuntimeException() { Object o = cs.cacheWithException("id", true); // See TestCacheInterceptor assertThat(o).isEqualTo(55L); } @Test - public void customInterceptorAppliesWithCheckedException() { + void customInterceptorAppliesWithCheckedException() { assertThatRuntimeException() - .isThrownBy(() -> cs.cacheWithCheckedException("id", true)) - .withCauseExactlyInstanceOf(IOException.class); + .isThrownBy(() -> cs.cacheWithCheckedException("id", true)) + .withCauseExactlyInstanceOf(IOException.class); } @@ -120,18 +128,28 @@ public Cache exceptionCache() { return new ConcurrentMapCache("exception"); } - @Bean - public JCacheInterceptor jCacheInterceptor(JCacheOperationSource cacheOperationSource) { - JCacheInterceptor cacheInterceptor = new TestCacheInterceptor(); - cacheInterceptor.setCacheOperationSource(cacheOperationSource); - return cacheInterceptor; - } } + static class CacheInterceptorBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + CacheInterceptorBeanPostProcessor(BeanFactory beanFactory) {this.beanFactory = beanFactory;} + + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (beanName.equals("jCacheInterceptor")) { + JCacheInterceptor cacheInterceptor = new TestCacheInterceptor(); + cacheInterceptor.setCacheOperationSource(beanFactory.getBean(JCacheOperationSource.class)); + return cacheInterceptor; + } + return bean; + } + + } + /** - * A test {@link org.springframework.cache.interceptor.CacheInterceptor} that handles special exception - * types. + * A test {@link JCacheInterceptor} that handles special exception types. */ @SuppressWarnings("serial") static class TestCacheInterceptor extends JCacheInterceptor { diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java index 1dcc12540d1e..4cdfa12cec62 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.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. @@ -52,7 +52,7 @@ /** * @author Stephane Nicoll */ -public class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { +class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { @@ -61,7 +61,7 @@ protected ApplicationContext getApplicationContext() { @Test - public void fullCachingConfig() throws Exception { + void fullCachingConfig() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(FullCachingConfig.class); @@ -75,7 +75,7 @@ public void fullCachingConfig() throws Exception { } @Test - public void emptyConfigSupport() { + void emptyConfigSupport() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(EmptyConfigSupportConfig.class); @@ -88,7 +88,7 @@ public void emptyConfigSupport() { } @Test - public void bothSetOnlyResolverIsUsed() { + void bothSetOnlyResolverIsUsed() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(FullCachingConfigSupport.class); @@ -100,7 +100,7 @@ public void bothSetOnlyResolverIsUsed() { } @Test - public void exceptionCacheResolverLazilyRequired() { + void exceptionCacheResolverLazilyRequired() { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(NoExceptionCacheResolverConfig.class)) { DefaultJCacheOperationSource cos = context.getBean(DefaultJCacheOperationSource.class); assertThat(cos.getCacheResolver()).isSameAs(context.getBean("cacheResolver")); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java index 262689f83ad8..2e4800fac7bc 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -31,7 +31,7 @@ /** * @author Stephane Nicoll */ -public class JCacheNamespaceDrivenTests extends AbstractJCacheAnnotationTests { +class JCacheNamespaceDrivenTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { @@ -40,7 +40,7 @@ protected ApplicationContext getApplicationContext() { } @Test - public void cacheResolver() { + void cacheResolver() { ConfigurableApplicationContext context = new GenericXmlApplicationContext( "/org/springframework/cache/jcache/config/jCacheNamespaceDriven-resolver.xml"); @@ -50,7 +50,7 @@ public void cacheResolver() { } @Test - public void testCacheErrorHandler() { + void testCacheErrorHandler() { JCacheInterceptor ci = ctx.getBean(JCacheInterceptor.class); assertThat(ci.getErrorHandler()).isSameAs(ctx.getBean("errorHandler", CacheErrorHandler.class)); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java index f07e5214850e..615498040a49 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 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,7 +23,7 @@ /** * @author Stephane Nicoll */ -public class JCacheStandaloneConfigTests extends AbstractJCacheAnnotationTests { +class JCacheStandaloneConfigTests extends AbstractJCacheAnnotationTests { @Override protected ApplicationContext getApplicationContext() { diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java index c56f336e35d2..140c1d519c8e 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.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. @@ -42,13 +42,10 @@ public abstract class AbstractCacheOperationTests> @Test - public void simple() { + void simple() { O operation = createSimpleOperation(); assertThat(operation.getCacheName()).as("Wrong cache name").isEqualTo("simpleCache"); - assertThat(operation.getAnnotations()).as("Unexpected number of annotation on " + operation.getMethod()) - .hasSize(1); - assertThat(operation.getAnnotations().iterator().next()).as("Wrong method annotation").isEqualTo(operation.getCacheAnnotation()); - + assertThat(operation.getAnnotations()).singleElement().isEqualTo(operation.getCacheAnnotation()); assertThat(operation.getCacheResolver()).as("cache resolver should be set").isNotNull(); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java index e85c27507862..4ced7de3d6a9 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.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. @@ -46,7 +46,7 @@ /** * @author Stephane Nicoll */ -public class AnnotationCacheOperationSourceTests extends AbstractJCacheTests { +class AnnotationCacheOperationSourceTests extends AbstractJCacheTests { private final DefaultJCacheOperationSource source = new DefaultJCacheOperationSource(); @@ -54,7 +54,7 @@ public class AnnotationCacheOperationSourceTests extends AbstractJCacheTests { @BeforeEach - public void setup() { + void setup() { source.setCacheResolver(defaultCacheResolver); source.setExceptionCacheResolver(defaultExceptionCacheResolver); source.setKeyGenerator(defaultKeyGenerator); @@ -63,14 +63,14 @@ public void setup() { @Test - public void cache() { + void cache() { CacheResultOperation op = getDefaultCacheOperation(CacheResultOperation.class, String.class); assertDefaults(op); assertThat(op.getExceptionCacheResolver()).as("Exception caching not enabled so resolver should not be set").isNull(); } @Test - public void cacheWithException() { + void cacheWithException() { CacheResultOperation op = getDefaultCacheOperation(CacheResultOperation.class, String.class, boolean.class); assertDefaults(op); assertThat(op.getExceptionCacheResolver()).isEqualTo(defaultExceptionCacheResolver); @@ -78,41 +78,41 @@ public void cacheWithException() { } @Test - public void put() { + void put() { CachePutOperation op = getDefaultCacheOperation(CachePutOperation.class, String.class, Object.class); assertDefaults(op); } @Test - public void remove() { + void remove() { CacheRemoveOperation op = getDefaultCacheOperation(CacheRemoveOperation.class, String.class); assertDefaults(op); } @Test - public void removeAll() { + void removeAll() { CacheRemoveAllOperation op = getDefaultCacheOperation(CacheRemoveAllOperation.class); assertThat(op.getCacheResolver()).isEqualTo(defaultCacheResolver); } @Test - public void noAnnotation() { + void noAnnotation() { assertThat(getCacheOperation(AnnotatedJCacheableService.class, this.cacheName)).isNull(); } @Test - public void multiAnnotations() { + void multiAnnotations() { assertThatIllegalStateException().isThrownBy(() -> getCacheOperation(InvalidCases.class, this.cacheName)); } @Test - public void defaultCacheNameWithCandidate() { + void defaultCacheNameWithCandidate() { Method method = ReflectionUtils.findMethod(Object.class, "toString"); assertThat(source.determineCacheName(method, null, "foo")).isEqualTo("foo"); } @Test - public void defaultCacheNameWithDefaults() { + void defaultCacheNameWithDefaults() { Method method = ReflectionUtils.findMethod(Object.class, "toString"); CacheDefaults mock = mock(); given(mock.cacheName()).willReturn(""); @@ -120,19 +120,19 @@ public void defaultCacheNameWithDefaults() { } @Test - public void defaultCacheNameNoDefaults() { + void defaultCacheNameNoDefaults() { Method method = ReflectionUtils.findMethod(Object.class, "toString"); assertThat(source.determineCacheName(method, null, "")).isEqualTo("java.lang.Object.toString()"); } @Test - public void defaultCacheNameWithParameters() { + void defaultCacheNameWithParameters() { Method method = ReflectionUtils.findMethod(Comparator.class, "compare", Object.class, Object.class); assertThat(source.determineCacheName(method, null, "")).isEqualTo("java.util.Comparator.compare(java.lang.Object,java.lang.Object)"); } @Test - public void customCacheResolver() { + void customCacheResolver() { CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, CustomService.class, this.cacheName, Long.class); assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); @@ -142,7 +142,7 @@ public void customCacheResolver() { } @Test - public void customKeyGenerator() { + void customKeyGenerator() { CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, CustomService.class, this.cacheName, Long.class); assertThat(operation.getCacheResolver()).isEqualTo(defaultCacheResolver); @@ -151,7 +151,7 @@ public void customKeyGenerator() { } @Test - public void customKeyGeneratorSpringBean() { + void customKeyGeneratorSpringBean() { TestableCacheKeyGenerator bean = new TestableCacheKeyGenerator(); beanFactory.registerSingleton("fooBar", bean); CacheResultOperation operation = @@ -164,7 +164,7 @@ public void customKeyGeneratorSpringBean() { } @Test - public void customKeyGeneratorAndCacheResolver() { + void customKeyGeneratorAndCacheResolver() { CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, CustomServiceWithDefaults.class, this.cacheName, Long.class); assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); @@ -173,7 +173,7 @@ public void customKeyGeneratorAndCacheResolver() { } @Test - public void customKeyGeneratorAndCacheResolverWithExceptionName() { + void customKeyGeneratorAndCacheResolverWithExceptionName() { CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, CustomServiceWithDefaults.class, this.cacheName, Long.class); assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java index dee2f07ba3d0..ce3437751a1f 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.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. @@ -31,7 +31,7 @@ /** * @author Stephane Nicoll */ -public class CachePutOperationTests extends AbstractCacheOperationTests { +class CachePutOperationTests extends AbstractCacheOperationTests { @Override protected CachePutOperation createSimpleOperation() { @@ -41,7 +41,7 @@ protected CachePutOperation createSimpleOperation() { } @Test - public void simplePut() { + void simplePut() { CachePutOperation operation = createSimpleOperation(); CacheInvocationParameter[] allParameters = operation.getAllParameters(2L, sampleInstance); @@ -55,7 +55,7 @@ public void simplePut() { } @Test - public void noCacheValue() { + void noCacheValue() { CacheMethodDetails methodDetails = create(CachePut.class, SampleObject.class, "noCacheValue", Long.class); @@ -64,7 +64,7 @@ public void noCacheValue() { } @Test - public void multiCacheValues() { + void multiCacheValues() { CacheMethodDetails methodDetails = create(CachePut.class, SampleObject.class, "multiCacheValues", Long.class, SampleObject.class, SampleObject.class); @@ -73,7 +73,7 @@ public void multiCacheValues() { } @Test - public void invokeWithWrongParameters() { + void invokeWithWrongParameters() { CachePutOperation operation = createSimpleOperation(); assertThatIllegalStateException().isThrownBy(() -> @@ -81,7 +81,7 @@ public void invokeWithWrongParameters() { } @Test - public void fullPutConfig() { + void fullPutConfig() { CacheMethodDetails methodDetails = create(CachePut.class, SampleObject.class, "fullPutConfig", Long.class, SampleObject.class); CachePutOperation operation = createDefaultOperation(methodDetails); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java index 083ae9048b14..e614b970e9d8 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.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. @@ -27,7 +27,7 @@ /** * @author Stephane Nicoll */ -public class CacheRemoveAllOperationTests extends AbstractCacheOperationTests { +class CacheRemoveAllOperationTests extends AbstractCacheOperationTests { @Override protected CacheRemoveAllOperation createSimpleOperation() { @@ -38,7 +38,7 @@ protected CacheRemoveAllOperation createSimpleOperation() { } @Test - public void simpleRemoveAll() { + void simpleRemoveAll() { CacheRemoveAllOperation operation = createSimpleOperation(); CacheInvocationParameter[] allParameters = operation.getAllParameters(); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java index 63c792d74c92..979edc1ec00d 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.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. @@ -27,7 +27,7 @@ /** * @author Stephane Nicoll */ -public class CacheRemoveOperationTests extends AbstractCacheOperationTests { +class CacheRemoveOperationTests extends AbstractCacheOperationTests { @Override protected CacheRemoveOperation createSimpleOperation() { @@ -38,7 +38,7 @@ protected CacheRemoveOperation createSimpleOperation() { } @Test - public void simpleRemove() { + void simpleRemove() { CacheRemoveOperation operation = createSimpleOperation(); CacheInvocationParameter[] allParameters = operation.getAllParameters(2L); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java index 9692d0e7edd9..1c6570009d2e 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.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. @@ -38,10 +38,10 @@ /** * @author Stephane Nicoll */ -public class CacheResolverAdapterTests extends AbstractJCacheTests { +class CacheResolverAdapterTests extends AbstractJCacheTests { @Test - public void resolveSimpleCache() throws Exception { + void resolveSimpleCache() throws Exception { DefaultCacheInvocationContext dummyContext = createDummyContext(); CacheResolverAdapter adapter = new CacheResolverAdapter(getCacheResolver(dummyContext, "testCache")); Collection caches = adapter.resolveCaches(dummyContext); @@ -51,7 +51,7 @@ public void resolveSimpleCache() throws Exception { } @Test - public void resolveUnknownCache() throws Exception { + void resolveUnknownCache() throws Exception { DefaultCacheInvocationContext dummyContext = createDummyContext(); CacheResolverAdapter adapter = new CacheResolverAdapter(getCacheResolver(dummyContext, null)); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java index 0764e1788e45..34477dfe9b3e 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.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 @@ /** * @author Stephane Nicoll */ -public class CacheResultOperationTests extends AbstractCacheOperationTests { +class CacheResultOperationTests extends AbstractCacheOperationTests { @Override protected CacheResultOperation createSimpleOperation() { @@ -47,7 +47,7 @@ protected CacheResultOperation createSimpleOperation() { } @Test - public void simpleGet() { + void simpleGet() { CacheResultOperation operation = createSimpleOperation(); assertThat(operation.getKeyGenerator()).isNotNull(); @@ -66,7 +66,7 @@ public void simpleGet() { } @Test - public void multiParameterKey() { + void multiParameterKey() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "multiKeysGet", Long.class, Boolean.class, String.class); CacheResultOperation operation = createDefaultOperation(methodDetails); @@ -78,7 +78,7 @@ public void multiParameterKey() { } @Test - public void invokeWithWrongParameters() { + void invokeWithWrongParameters() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "anotherSimpleGet", String.class, Long.class); CacheResultOperation operation = createDefaultOperation(methodDetails); @@ -89,7 +89,7 @@ public void invokeWithWrongParameters() { } @Test - public void tooManyKeyValues() { + void tooManyKeyValues() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "anotherSimpleGet", String.class, Long.class); CacheResultOperation operation = createDefaultOperation(methodDetails); @@ -100,7 +100,7 @@ public void tooManyKeyValues() { } @Test - public void annotatedGet() { + void annotatedGet() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "annotatedGet", Long.class, String.class); CacheResultOperation operation = createDefaultOperation(methodDetails); @@ -116,7 +116,7 @@ public void annotatedGet() { } @Test - public void fullGetConfig() { + void fullGetConfig() { CacheMethodDetails methodDetails = create(CacheResult.class, SampleObject.class, "fullGetConfig", Long.class); CacheResultOperation operation = createDefaultOperation(methodDetails); diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java index 3626d9f90f21..b26719570c0d 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.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. @@ -49,7 +49,7 @@ /** * @author Stephane Nicoll */ -public class JCacheErrorHandlerTests { +class JCacheErrorHandlerTests { private Cache cache; @@ -61,7 +61,7 @@ public class JCacheErrorHandlerTests { @BeforeEach - public void setup() { + void setup() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); this.cache = context.getBean("mockCache", Cache.class); this.errorCache = context.getBean("mockErrorCache", Cache.class); @@ -72,7 +72,7 @@ public void setup() { @Test - public void getFail() { + void getFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); Object key = SimpleKeyGenerator.generateKey(0L); willThrow(exception).given(this.cache).get(key); @@ -82,7 +82,7 @@ public void getFail() { } @Test - public void getPutNewElementFail() { + void getPutNewElementFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); Object key = SimpleKeyGenerator.generateKey(0L); given(this.cache.get(key)).willReturn(null); @@ -93,7 +93,7 @@ public void getPutNewElementFail() { } @Test - public void getFailPutExceptionFail() { + void getFailPutExceptionFail() { UnsupportedOperationException exceptionOnPut = new UnsupportedOperationException("Test exception on put"); Object key = SimpleKeyGenerator.generateKey(0L); given(this.cache.get(key)).willReturn(null); @@ -110,7 +110,7 @@ public void getFailPutExceptionFail() { } @Test - public void putFail() { + void putFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); Object key = SimpleKeyGenerator.generateKey(0L); willThrow(exception).given(this.cache).put(key, 234L); @@ -120,7 +120,7 @@ public void putFail() { } @Test - public void evictFail() { + void evictFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); Object key = SimpleKeyGenerator.generateKey(0L); willThrow(exception).given(this.cache).evict(key); @@ -130,7 +130,7 @@ public void evictFail() { } @Test - public void clearFail() { + void clearFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); willThrow(exception).given(this.cache).clear(); @@ -183,7 +183,7 @@ public static class SimpleService { private static final IllegalStateException TEST_EXCEPTION = new IllegalStateException("Test exception"); - private AtomicLong counter = new AtomicLong(); + private final AtomicLong counter = new AtomicLong(); @CacheResult public Object get(long id) { diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java index 0c6671ebbb07..9b99bfb503a4 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,12 +35,12 @@ /** * @author Stephane Nicoll */ -public class JCacheInterceptorTests extends AbstractJCacheTests { +class JCacheInterceptorTests extends AbstractJCacheTests { private final CacheOperationInvoker dummyInvoker = new DummyInvoker(null); @Test - public void severalCachesNotSupported() { + void severalCachesNotSupported() { JCacheInterceptor interceptor = createInterceptor(createOperationSource( cacheManager, new NamedCacheResolver(cacheManager, "default", "simpleCache"), defaultExceptionCacheResolver, defaultKeyGenerator)); @@ -54,7 +54,7 @@ cacheManager, new NamedCacheResolver(cacheManager, "default", "simpleCache"), } @Test - public void noCacheCouldBeResolved() { + void noCacheCouldBeResolved() { JCacheInterceptor interceptor = createInterceptor(createOperationSource( cacheManager, new NamedCacheResolver(cacheManager), // Returns empty list defaultExceptionCacheResolver, defaultKeyGenerator)); @@ -67,18 +67,18 @@ cacheManager, new NamedCacheResolver(cacheManager), // Returns empty list } @Test - public void cacheManagerMandatoryIfCacheResolverNotSet() { + void cacheManagerMandatoryIfCacheResolverNotSet() { assertThatIllegalStateException().isThrownBy(() -> createOperationSource(null, null, null, defaultKeyGenerator)); } @Test - public void cacheManagerOptionalIfCacheResolversSet() { + void cacheManagerOptionalIfCacheResolversSet() { createOperationSource(null, defaultCacheResolver, defaultExceptionCacheResolver, defaultKeyGenerator); } @Test - public void cacheResultReturnsProperType() throws Throwable { + void cacheResultReturnsProperType() { JCacheInterceptor interceptor = createInterceptor(createOperationSource( cacheManager, defaultCacheResolver, defaultExceptionCacheResolver, defaultKeyGenerator)); 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 35db912df971..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-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. @@ -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; @@ -44,7 +43,7 @@ /** * @author Stephane Nicoll */ -public class JCacheKeyGeneratorTests { +class JCacheKeyGeneratorTests { private TestKeyGenerator keyGenerator; @@ -53,7 +52,7 @@ public class JCacheKeyGeneratorTests { private Cache cache; @BeforeEach - public void setup() { + void setup() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); this.keyGenerator = context.getBean(TestKeyGenerator.class); this.simpleService = context.getBean(SimpleService.class); @@ -62,7 +61,7 @@ public void setup() { } @Test - public void getSimple() { + void getSimple() { this.keyGenerator.expect(1L); Object first = this.simpleService.get(1L); Object second = this.simpleService.get(1L); @@ -73,7 +72,7 @@ public void getSimple() { } @Test - public void getFlattenVararg() { + void getFlattenVararg() { this.keyGenerator.expect(1L, "foo", "bar"); Object first = this.simpleService.get(1L, "foo", "bar"); Object second = this.simpleService.get(1L, "foo", "bar"); @@ -84,7 +83,7 @@ public void getFlattenVararg() { } @Test - public void getFiltered() { + void getFiltered() { this.keyGenerator.expect(1L); Object first = this.simpleService.getFiltered(1L, "foo", "bar"); Object second = this.simpleService.getFiltered(1L, "foo", "bar"); @@ -120,7 +119,7 @@ public SimpleService simpleService() { @CacheDefaults(cacheName = "test") public static class SimpleService { - private AtomicLong counter = new AtomicLong(); + private final AtomicLong counter = new AtomicLong(); @CacheResult public Object get(long id) { @@ -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-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java index cf0487eb71cb..c5b03cf26bc1 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -71,18 +71,18 @@ void trackCacheName(TestInfo testInfo) { @Test - public void getOnExistingCache() { + void getOnExistingCache() { assertThat(getCacheManager(false).getCache(CACHE_NAME)).isInstanceOf(getCacheType()); } @Test - public void getOnNewCache() { + void getOnNewCache() { T cacheManager = getCacheManager(false); addNativeCache(this.cacheName); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isFalse(); + assertThat(cacheManager.getCacheNames()).doesNotContain(this.cacheName); try { assertThat(cacheManager.getCache(this.cacheName)).isInstanceOf(getCacheType()); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isTrue(); + assertThat(cacheManager.getCacheNames()).contains(this.cacheName); } finally { removeNativeCache(this.cacheName); @@ -90,27 +90,27 @@ public void getOnNewCache() { } @Test - public void getOnUnknownCache() { + void getOnUnknownCache() { T cacheManager = getCacheManager(false); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isFalse(); + assertThat(cacheManager.getCacheNames()).doesNotContain(this.cacheName); assertThat(cacheManager.getCache(this.cacheName)).isNull(); } @Test - public void getTransactionalOnExistingCache() { + void getTransactionalOnExistingCache() { assertThat(getCacheManager(true).getCache(CACHE_NAME)) .isInstanceOf(TransactionAwareCacheDecorator.class); } @Test - public void getTransactionalOnNewCache() { + void getTransactionalOnNewCache() { T cacheManager = getCacheManager(true); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isFalse(); + assertThat(cacheManager.getCacheNames()).doesNotContain(this.cacheName); addNativeCache(this.cacheName); try { assertThat(cacheManager.getCache(this.cacheName)) .isInstanceOf(TransactionAwareCacheDecorator.class); - assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isTrue(); + assertThat(cacheManager.getCacheNames()).contains(this.cacheName); } finally { removeNativeCache(this.cacheName); diff --git a/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java index 66369edfe6c0..dd4adb1c0730 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,25 +30,25 @@ * @author Stephane Nicoll * @author Juergen Hoeller */ -public class TransactionAwareCacheDecoratorTests { +class TransactionAwareCacheDecoratorTests { private final TransactionTemplate txTemplate = new TransactionTemplate(new CallCountingTransactionManager()); @Test - public void createWithNullTarget() { + void createWithNullTarget() { assertThatIllegalArgumentException().isThrownBy(() -> new TransactionAwareCacheDecorator(null)); } @Test - public void getTargetCache() { + void getTargetCache() { Cache target = new ConcurrentMapCache("testCache"); TransactionAwareCacheDecorator cache = new TransactionAwareCacheDecorator(target); assertThat(cache.getTargetCache()).isSameAs(target); } @Test - public void regularOperationsOnTarget() { + void regularOperationsOnTarget() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); assertThat(cache.getName()).isEqualTo(target.getName()); @@ -64,7 +64,7 @@ public void regularOperationsOnTarget() { } @Test - public void putNonTransactional() { + void putNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); @@ -74,7 +74,7 @@ public void putNonTransactional() { } @Test - public void putTransactional() { + void putTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -88,7 +88,7 @@ public void putTransactional() { } @Test - public void putIfAbsentNonTransactional() { + void putIfAbsentNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); @@ -101,7 +101,7 @@ public void putIfAbsentNonTransactional() { } @Test - public void putIfAbsentTransactional() { // no transactional support for putIfAbsent + void putIfAbsentTransactional() { // no transactional support for putIfAbsent Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -118,7 +118,7 @@ public void putIfAbsentTransactional() { // no transactional support for putIfA } @Test - public void evictNonTransactional() { + void evictNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -129,7 +129,7 @@ public void evictNonTransactional() { } @Test - public void evictTransactional() { + void evictTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -144,7 +144,7 @@ public void evictTransactional() { } @Test - public void evictIfPresentNonTransactional() { + void evictIfPresentNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -155,7 +155,7 @@ public void evictIfPresentNonTransactional() { } @Test - public void evictIfPresentTransactional() { // no transactional support for evictIfPresent + void evictIfPresentTransactional() { // no transactional support for evictIfPresent Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -170,7 +170,7 @@ public void evictIfPresentTransactional() { // no transactional support for evi } @Test - public void clearNonTransactional() { + void clearNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -181,7 +181,7 @@ public void clearNonTransactional() { } @Test - public void clearTransactional() { + void clearTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -196,7 +196,7 @@ public void clearTransactional() { } @Test - public void invalidateNonTransactional() { + void invalidateNonTransactional() { Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); @@ -207,7 +207,7 @@ public void invalidateNonTransactional() { } @Test - public void invalidateTransactional() { // no transactional support for invalidate + void invalidateTransactional() { // no transactional support for invalidate Cache target = new ConcurrentMapCache("testCache"); Cache cache = new TransactionAwareCacheDecorator(target); Object key = new Object(); diff --git a/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java b/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java index 5312152ce4ce..7fd2cfe272bb 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -32,10 +32,10 @@ * @author Chris Beams * @since 10.09.2003 */ -public class SimpleMailMessageTests { +class SimpleMailMessageTests { @Test - public void testSimpleMessageCopyCtor() { + void testSimpleMessageCopyCtor() { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom("me@mail.org"); message.setTo("you@mail.org"); @@ -45,8 +45,8 @@ public void testSimpleMessageCopyCtor() { assertThat(messageCopy.getTo()[0]).isEqualTo("you@mail.org"); message.setReplyTo("reply@mail.org"); - message.setCc(new String[]{"he@mail.org", "she@mail.org"}); - message.setBcc(new String[]{"us@mail.org", "them@mail.org"}); + message.setCc("he@mail.org", "she@mail.org"); + message.setBcc("us@mail.org", "them@mail.org"); Date sentDate = new Date(); message.setSentDate(sentDate); message.setSubject("my subject"); @@ -56,11 +56,11 @@ public void testSimpleMessageCopyCtor() { assertThat(message.getReplyTo()).isEqualTo("reply@mail.org"); assertThat(message.getTo()[0]).isEqualTo("you@mail.org"); List ccs = Arrays.asList(message.getCc()); - assertThat(ccs.contains("he@mail.org")).isTrue(); - assertThat(ccs.contains("she@mail.org")).isTrue(); + assertThat(ccs).contains("he@mail.org"); + assertThat(ccs).contains("she@mail.org"); List bccs = Arrays.asList(message.getBcc()); - assertThat(bccs.contains("us@mail.org")).isTrue(); - assertThat(bccs.contains("them@mail.org")).isTrue(); + assertThat(bccs).contains("us@mail.org"); + assertThat(bccs).contains("them@mail.org"); assertThat(message.getSentDate()).isEqualTo(sentDate); assertThat(message.getSubject()).isEqualTo("my subject"); assertThat(message.getText()).isEqualTo("my text"); @@ -70,23 +70,23 @@ public void testSimpleMessageCopyCtor() { assertThat(messageCopy.getReplyTo()).isEqualTo("reply@mail.org"); assertThat(messageCopy.getTo()[0]).isEqualTo("you@mail.org"); ccs = Arrays.asList(messageCopy.getCc()); - assertThat(ccs.contains("he@mail.org")).isTrue(); - assertThat(ccs.contains("she@mail.org")).isTrue(); + assertThat(ccs).contains("he@mail.org"); + assertThat(ccs).contains("she@mail.org"); bccs = Arrays.asList(message.getBcc()); - assertThat(bccs.contains("us@mail.org")).isTrue(); - assertThat(bccs.contains("them@mail.org")).isTrue(); + assertThat(bccs).contains("us@mail.org"); + assertThat(bccs).contains("them@mail.org"); assertThat(messageCopy.getSentDate()).isEqualTo(sentDate); assertThat(messageCopy.getSubject()).isEqualTo("my subject"); assertThat(messageCopy.getText()).isEqualTo("my text"); } @Test - public void testDeepCopyOfStringArrayTypedFieldsOnCopyCtor() throws Exception { + void testDeepCopyOfStringArrayTypedFieldsOnCopyCtor() { SimpleMailMessage original = new SimpleMailMessage(); - original.setTo(new String[]{"fiona@mail.org", "apple@mail.org"}); - original.setCc(new String[]{"he@mail.org", "she@mail.org"}); - original.setBcc(new String[]{"us@mail.org", "them@mail.org"}); + original.setTo("fiona@mail.org", "apple@mail.org"); + original.setCc("he@mail.org", "she@mail.org"); + original.setBcc("us@mail.org", "them@mail.org"); SimpleMailMessage copy = new SimpleMailMessage(original); @@ -121,6 +121,7 @@ public final void testHashCode() { assertThat(message2.hashCode()).isEqualTo(message1.hashCode()); } + @Test public final void testEqualsObject() { SimpleMailMessage message1; SimpleMailMessage message2; @@ -128,7 +129,7 @@ public final void testEqualsObject() { // Same object is equal message1 = new SimpleMailMessage(); message2 = message1; - assertThat(message1.equals(message2)).isTrue(); + assertThat(message1).isEqualTo(message2); // Null object is not equal message1 = new SimpleMailMessage(); @@ -143,7 +144,7 @@ public final void testEqualsObject() { // Equal values are equal message1 = new SimpleMailMessage(); message2 = new SimpleMailMessage(); - assertThat(message1.equals(message2)).isTrue(); + assertThat(message1).isEqualTo(message2); message1 = new SimpleMailMessage(); message1.setFrom("from@somewhere"); @@ -155,17 +156,17 @@ public final void testEqualsObject() { message1.setSubject("subject"); message1.setText("text"); message2 = new SimpleMailMessage(message1); - assertThat(message1.equals(message2)).isTrue(); + assertThat(message1).isEqualTo(message2); } @Test - public void testCopyCtorChokesOnNullOriginalMessage() throws Exception { + void testCopyCtorChokesOnNullOriginalMessage() { assertThatIllegalArgumentException().isThrownBy(() -> new SimpleMailMessage(null)); } @Test - public void testCopyToChokesOnNullTargetMessage() throws Exception { + void testCopyToChokesOnNullTargetMessage() { assertThatIllegalArgumentException().isThrownBy(() -> new SimpleMailMessage().copyTo(null)); } diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java index ebbf493cd5f0..143856494189 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,10 +29,10 @@ * @author Rob Harrop * @author Juergen Hoeller */ -public class ConfigurableMimeFileTypeMapTests { +class ConfigurableMimeFileTypeMapTests { @Test - public void againstDefaultConfiguration() throws Exception { + void againstDefaultConfiguration() { ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); ftm.afterPropertiesSet(); @@ -45,15 +45,15 @@ public void againstDefaultConfiguration() throws Exception { } @Test - public void againstDefaultConfigurationWithFilePath() throws Exception { + void againstDefaultConfigurationWithFilePath() { ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); assertThat(ftm.getContentType(new File("/tmp/foobar.HTM"))).as("Invalid content type for HTM").isEqualTo("text/html"); } @Test - public void withAdditionalMappings() throws Exception { + void withAdditionalMappings() { ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); - ftm.setMappings(new String[] {"foo/bar HTM foo", "foo/cpp c++"}); + ftm.setMappings("foo/bar HTM foo", "foo/cpp c++"); ftm.afterPropertiesSet(); assertThat(ftm.getContentType("foobar.HTM")).as("Invalid content type for HTM - override didn't work").isEqualTo("foo/bar"); @@ -62,7 +62,7 @@ public void withAdditionalMappings() throws Exception { } @Test - public void withCustomMappingLocation() throws Exception { + void withCustomMappingLocation() { Resource resource = new ClassPathResource("test.mime.types", getClass()); ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java index cb321a09ecbd..35d062615594 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.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,7 +26,7 @@ * @author Sam Brannen * @since 09.07.2005 */ -public class InternetAddressEditorTests { +class InternetAddressEditorTests { private static final String EMPTY = ""; private static final String SIMPLE = "nobody@nowhere.com"; @@ -36,42 +36,42 @@ public class InternetAddressEditorTests { @Test - public void uninitialized() { + void uninitialized() { assertThat(editor.getAsText()).as("Uninitialized editor did not return empty value string").isEmpty(); } @Test - public void setNull() { + void setNull() { editor.setAsText(null); assertThat(editor.getAsText()).as("Setting null did not result in empty value string").isEmpty(); } @Test - public void setEmpty() { + void setEmpty() { editor.setAsText(EMPTY); assertThat(editor.getAsText()).as("Setting empty string did not result in empty value string").isEmpty(); } @Test - public void allWhitespace() { + void allWhitespace() { editor.setAsText(" "); assertThat(editor.getAsText()).as("All whitespace was not recognized").isEmpty(); } @Test - public void simpleGoodAddress() { + void simpleGoodAddress() { editor.setAsText(SIMPLE); assertThat(editor.getAsText()).as("Simple email address failed").isEqualTo(SIMPLE); } @Test - public void excessWhitespace() { + void excessWhitespace() { editor.setAsText(" " + SIMPLE + " "); assertThat(editor.getAsText()).as("Whitespace was not stripped").isEqualTo(SIMPLE); } @Test - public void simpleBadAddress() { + void simpleBadAddress() { assertThatIllegalArgumentException().isThrownBy(() -> editor.setAsText(BAD)); } diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java index 8daf7d1b35c6..6dcb936df872 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.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.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; @@ -38,6 +39,7 @@ import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.Test; +import org.springframework.core.io.ByteArrayResource; import org.springframework.mail.MailParseException; import org.springframework.mail.MailSendException; import org.springframework.mail.SimpleMailMessage; @@ -72,7 +74,7 @@ void javaMailSenderWithSimpleMessage() throws Exception { simpleMessage.setTo("you@mail.org"); simpleMessage.setCc("he@mail.org", "she@mail.org"); simpleMessage.setBcc("us@mail.org", "them@mail.org"); - Date sentDate = new GregorianCalendar(2004, 1, 1).getTime(); + Date sentDate = new GregorianCalendar(2004, Calendar.FEBRUARY, 1).getTime(); simpleMessage.setSentDate(sentDate); simpleMessage.setSubject("my subject"); simpleMessage.setText("my text"); @@ -269,6 +271,25 @@ void javaMailSenderWithMimeMessageHelperAndDefaultEncoding() throws Exception { assertThat(sender.transport.getSentMessages()).containsExactly(message.getMimeMessage()); } + @Test + void javaMailSenderWithMimeMessageHelperAndCustomResource() throws Exception { + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + MimeMessageHelper message = new MimeMessageHelper(sender.createMimeMessage(), true); + message.setTo("you@mail.org"); + message.addInline("id", new ByteArrayResource(new byte[] {1, 2, 3})); + + sender.send(message.getMimeMessage()); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages()).containsExactly(message.getMimeMessage()); + } + @Test void javaMailSenderWithParseExceptionOnSimpleMessage() { SimpleMailMessage simpleMessage = new SimpleMailMessage(); @@ -305,7 +326,7 @@ protected Transport getTransport(Session sess) throws NoSuchProviderException { MimeMessage mimeMessage = sender.createMimeMessage(); mimeMessage.setSubject("custom"); mimeMessage.setRecipient(RecipientType.TO, new InternetAddress("you@mail.org")); - mimeMessage.setSentDate(new GregorianCalendar(2005, 3, 1).getTime()); + mimeMessage.setSentDate(new GregorianCalendar(2005, Calendar.APRIL, 1).getTime()); sender.send(mimeMessage); assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); @@ -386,9 +407,9 @@ void failedSimpleMessage() throws Exception { assertThat(sender.transport.isCloseCalled()).isTrue(); assertThat(sender.transport.getSentMessages()).hasSize(1); assertThat(sender.transport.getSentMessage(0).getAllRecipients()[0]).isEqualTo(new InternetAddress("she@mail.org")); - assertThat(ex.getFailedMessages().keySet()).containsExactly(simpleMessage1); - Exception subEx = ex.getFailedMessages().values().iterator().next(); - assertThat(subEx).isInstanceOf(MessagingException.class).hasMessage("failed"); + assertThat(ex.getFailedMessages()).containsOnlyKeys(simpleMessage1); + assertThat(ex.getFailedMessages().get(simpleMessage1)) + .isInstanceOf(MessagingException.class).hasMessage("failed"); } } @@ -413,9 +434,9 @@ void failedMimeMessage() throws Exception { assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); assertThat(sender.transport.isCloseCalled()).isTrue(); assertThat(sender.transport.getSentMessages()).containsExactly(mimeMessage2); - assertThat(ex.getFailedMessages().keySet()).containsExactly(mimeMessage1); - Exception subEx = ex.getFailedMessages().values().iterator().next(); - assertThat(subEx).isInstanceOf(MessagingException.class).hasMessage("failed"); + assertThat(ex.getFailedMessages()).containsOnlyKeys(mimeMessage1); + assertThat(ex.getFailedMessages().get(mimeMessage1)) + .isInstanceOf(MessagingException.class).hasMessage("failed"); } } @@ -456,7 +477,7 @@ private static class MockTransport extends Transport { private String connectedUsername = null; private String connectedPassword = null; private boolean closeCalled = false; - private List sentMessages = new ArrayList<>(); + private final List sentMessages = new ArrayList<>(); private MockTransport(Session session, URLName urlName) { super(session, urlName); @@ -504,7 +525,7 @@ public void connect(String host, int port, String username, String password) thr @Override public synchronized void close() throws MessagingException { - if ("".equals(connectedHost)) { + if (this.connectedHost.isEmpty()) { throw new MessagingException("close failure"); } this.closeCalled = true; @@ -523,7 +544,7 @@ public void sendMessage(Message message, Address[] addresses) throws MessagingEx throw new MessagingException("No sentDate specified"); } if (message.getSubject() != null && message.getSubject().contains("custom")) { - assertThat(message.getSentDate()).isEqualTo(new GregorianCalendar(2005, 3, 1).getTime()); + assertThat(message.getSentDate()).isEqualTo(new GregorianCalendar(2005, Calendar.APRIL, 1).getTime()); } this.sentMessages.add(message); } diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java index 31426b1f6be5..7d1af052ae78 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ * @author Mark Fisher * @since 3.0 */ -public class QuartzSchedulerLifecycleTests { +class QuartzSchedulerLifecycleTests { @Test // SPR-6354 public void destroyLazyInitSchedulerWithDefaultShutdownOrderDoesNotHang() { diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java index 35ea29c98e67..87d60ab0a009 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.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. @@ -25,7 +25,6 @@ import org.junit.jupiter.api.Test; import org.quartz.Job; import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; import org.quartz.Scheduler; import org.quartz.SchedulerContext; import org.quartz.SchedulerFactory; @@ -352,7 +351,6 @@ void schedulerAccessorBean() throws Exception { } @Test - @SuppressWarnings("resource") void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); context.registerBeanDefinition("scheduler", new RootBeanDefinition(SchedulerFactoryBean.class)); @@ -363,7 +361,6 @@ void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception { } @Test - @SuppressWarnings("resource") void schedulerAutoStartupFalse() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class) @@ -376,7 +373,7 @@ void schedulerAutoStartupFalse() throws Exception { } @Test - void schedulerRepositoryExposure() throws Exception { + void schedulerRepositoryExposure() { try (ClassPathXmlApplicationContext ctx = context("schedulerRepositoryExposure.xml")) { assertThat(ctx.getBean("scheduler")).isSameAs(SchedulerRepository.getInstance().lookup("myScheduler")); } @@ -387,23 +384,17 @@ void schedulerRepositoryExposure() throws Exception { * TODO: Against Quartz 2.2, this test's job doesn't actually execute anymore... */ @Test - void schedulerWithHsqlDataSource() throws Exception { + void schedulerWithHsqlDataSource() { DummyJob.param = 0; DummyJob.count = 0; try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) { JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class)); assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse(); - - /* - Thread.sleep(3000); - assertTrue("DummyJob should have been executed at least once.", DummyJob.count > 0); - */ } } @Test - @SuppressWarnings("resource") void schedulerFactoryBeanWithCustomJobStore() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); @@ -459,7 +450,7 @@ public void setParam(int value) { } @Override - public synchronized void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + public synchronized void execute(JobExecutionContext jobExecutionContext) { count++; } } @@ -480,7 +471,7 @@ public void setParam(int value) { } @Override - protected synchronized void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { + protected synchronized void executeInternal(JobExecutionContext jobExecutionContext) { count++; } } diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java index 7e422ed9b331..0f3fd26dd967 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHintsTests.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. @@ -34,7 +34,7 @@ * * @author Sebastien Deleuze */ -public class SchedulerFactoryBeanRuntimeHintsTests { +class SchedulerFactoryBeanRuntimeHintsTests { private final RuntimeHints hints = new RuntimeHints(); diff --git a/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java b/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java index c0a07e448a0c..0ab2045390fd 100644 --- a/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java +++ b/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,12 +39,12 @@ * @author Issam El-atif * @author Sam Brannen */ -public class FreeMarkerConfigurationFactoryBeanTests { +class FreeMarkerConfigurationFactoryBeanTests { private final FreeMarkerConfigurationFactoryBean fcfb = new FreeMarkerConfigurationFactoryBean(); @Test - public void freeMarkerConfigurationFactoryBeanWithConfigLocation() throws Exception { + void freeMarkerConfigurationFactoryBeanWithConfigLocation() { fcfb.setConfigLocation(new FileSystemResource("myprops.properties")); Properties props = new Properties(); props.setProperty("myprop", "/mydir"); @@ -53,7 +53,7 @@ public void freeMarkerConfigurationFactoryBeanWithConfigLocation() throws Except } @Test - public void freeMarkerConfigurationFactoryBeanWithResourceLoaderPath() throws Exception { + void freeMarkerConfigurationFactoryBeanWithResourceLoaderPath() throws Exception { fcfb.setTemplateLoaderPath("file:/mydir"); fcfb.afterPropertiesSet(); Configuration cfg = fcfb.getObject(); diff --git a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java index b1fc5cda8bff..b8892b536cf3 100644 --- a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java +++ b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -54,7 +54,7 @@ public abstract class AbstractJCacheAnnotationTests { protected abstract ApplicationContext getApplicationContext(); @BeforeEach - public void setUp(TestInfo testInfo) { + protected void setUp(TestInfo testInfo) { this.keyItem = testInfo.getTestMethod().get().getName(); this.ctx = getApplicationContext(); this.service = this.ctx.getBean(JCacheableService.class); @@ -62,14 +62,14 @@ public void setUp(TestInfo testInfo) { } @Test - public void cache() { + protected void cache() { Object first = service.cache(this.keyItem); Object second = service.cache(this.keyItem); assertThat(second).isSameAs(first); } @Test - public void cacheNull() { + protected void cacheNull() { Cache cache = getCache(DEFAULT_CACHE); assertThat(cache.get(this.keyItem)).isNull(); @@ -85,7 +85,7 @@ public void cacheNull() { } @Test - public void cacheException() { + protected void cacheException() { Cache cache = getCache(EXCEPTION_CACHE); Object key = createKey(this.keyItem); @@ -100,7 +100,7 @@ public void cacheException() { } @Test - public void cacheExceptionVetoed() { + protected void cacheExceptionVetoed() { Cache cache = getCache(EXCEPTION_CACHE); Object key = createKey(this.keyItem); @@ -112,7 +112,7 @@ public void cacheExceptionVetoed() { } @Test - public void cacheCheckedException() { + protected void cacheCheckedException() { Cache cache = getCache(EXCEPTION_CACHE); Object key = createKey(this.keyItem); @@ -128,7 +128,7 @@ public void cacheCheckedException() { @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test - public void cacheExceptionRewriteCallStack() { + protected void cacheExceptionRewriteCallStack() { long ref = service.exceptionInvocations(); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> service.cacheWithException(this.keyItem, true)) @@ -151,14 +151,14 @@ public void cacheExceptionRewriteCallStack() { } @Test - public void cacheAlwaysInvoke() { + protected void cacheAlwaysInvoke() { Object first = service.cacheAlwaysInvoke(this.keyItem); Object second = service.cacheAlwaysInvoke(this.keyItem); assertThat(second).isNotSameAs(first); } @Test - public void cacheWithPartialKey() { + protected void cacheWithPartialKey() { Object first = service.cacheWithPartialKey(this.keyItem, true); Object second = service.cacheWithPartialKey(this.keyItem, false); // second argument not used, see config @@ -166,7 +166,7 @@ public void cacheWithPartialKey() { } @Test - public void cacheWithCustomCacheResolver() { + protected void cacheWithCustomCacheResolver() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -177,7 +177,7 @@ public void cacheWithCustomCacheResolver() { } @Test - public void cacheWithCustomKeyGenerator() { + protected void cacheWithCustomKeyGenerator() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -187,7 +187,7 @@ public void cacheWithCustomKeyGenerator() { } @Test - public void put() { + protected void put() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -202,7 +202,7 @@ public void put() { } @Test - public void putWithException() { + protected void putWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -218,7 +218,7 @@ public void putWithException() { } @Test - public void putWithExceptionVetoPut() { + protected void putWithExceptionVetoPut() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -231,7 +231,7 @@ public void putWithExceptionVetoPut() { } @Test - public void earlyPut() { + protected void earlyPut() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -246,7 +246,7 @@ public void earlyPut() { } @Test - public void earlyPutWithException() { + protected void earlyPutWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -262,7 +262,7 @@ public void earlyPutWithException() { } @Test - public void earlyPutWithExceptionVetoPut() { + protected void earlyPutWithExceptionVetoPut() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -277,7 +277,7 @@ public void earlyPutWithExceptionVetoPut() { } @Test - public void remove() { + protected void remove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -290,7 +290,7 @@ public void remove() { } @Test - public void removeWithException() { + protected void removeWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -304,7 +304,7 @@ public void removeWithException() { } @Test - public void removeWithExceptionVetoRemove() { + protected void removeWithExceptionVetoRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -319,7 +319,7 @@ public void removeWithExceptionVetoRemove() { } @Test - public void earlyRemove() { + protected void earlyRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -332,7 +332,7 @@ public void earlyRemove() { } @Test - public void earlyRemoveWithException() { + protected void earlyRemoveWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -345,7 +345,7 @@ public void earlyRemoveWithException() { } @Test - public void earlyRemoveWithExceptionVetoRemove() { + protected void earlyRemoveWithExceptionVetoRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -359,7 +359,7 @@ public void earlyRemoveWithExceptionVetoRemove() { } @Test - public void removeAll() { + protected void removeAll() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -371,7 +371,7 @@ public void removeAll() { } @Test - public void removeAllWithException() { + protected void removeAllWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -384,7 +384,7 @@ public void removeAllWithException() { } @Test - public void removeAllWithExceptionVetoRemove() { + protected void removeAllWithExceptionVetoRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -396,7 +396,7 @@ public void removeAllWithExceptionVetoRemove() { } @Test - public void earlyRemoveAll() { + protected void earlyRemoveAll() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -408,7 +408,7 @@ public void earlyRemoveAll() { } @Test - public void earlyRemoveAllWithException() { + protected void earlyRemoveAllWithException() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); @@ -420,7 +420,7 @@ public void earlyRemoveAllWithException() { } @Test - public void earlyRemoveAllWithExceptionVetoRemove() { + protected void earlyRemoveAllWithExceptionVetoRemove() { Cache cache = getCache(DEFAULT_CACHE); Object key = createKey(this.keyItem); diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index e8299dcc63c5..a19b2f314da6 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -12,7 +12,9 @@ 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") optional("jakarta.annotation:jakarta.annotation-api") optional("jakarta.ejb:jakarta.ejb-api") @@ -20,21 +22,19 @@ 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")) testImplementation(testFixtures(project(":spring-aop"))) diff --git a/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java b/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java index 51a568ca30a1..8fb9581b1e00 100644 --- a/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java +++ b/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -57,8 +57,8 @@ public void setup() { @TearDown public void teardown() { - System.getProperties().remove("country"); - System.getProperties().remove("name"); + System.clearProperty("country"); + System.clearProperty("name"); } } 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 63f93f32ea41..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,14 +20,14 @@ 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. * - *

    Serves as an SPI for Spring's annotation-based caching model - * ({@link org.springframework.cache.annotation.Cacheable} and co) - * as well as an API for direct usage in applications. + *

    Serves primarily as an SPI for Spring's annotation-based caching + * model ({@link org.springframework.cache.annotation.Cacheable} and co) + * and secondarily as an API for direct usage in applications. * *

    Note: Due to the generic use of caching, it is recommended * that implementations allow storage of {@code null} values @@ -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,27 +103,37 @@ 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, * wrapped in a {@link CompletableFuture}. This operation must not block * but is allowed to return a completed {@link CompletableFuture} if the * corresponding value is immediately available. - *

    Returns {@code null} if the cache contains no mapping for this key; - * otherwise, the cached value (which may be {@code null} itself) will - * be returned in the {@link CompletableFuture}. + *

    Can return {@code null} if the cache can immediately determine that + * it contains no mapping for this key (for example, through an in-memory key map). + * Otherwise, the cached value will be returned in the {@link CompletableFuture}, + * with {@code null} indicating a late-determined cache miss. A nested + * {@link ValueWrapper} potentially indicates a nullable cached value; + * the cached value may also be represented as a plain element if null + * values are not supported. Calling code needs to be prepared to handle + * all those variants of the result returned by this method. * @param key the key whose associated value is to be returned - * @return the value to which this cache maps the specified key, - * contained within a {@link CompletableFuture} which may also hold - * a cached {@code null} value. A straight {@code null} being - * returned means that the cache contains no mapping for this key. + * @return the value to which this cache maps the specified key, contained + * within a {@link CompletableFuture} which may also be empty when a cache + * miss has been late-determined. A straight {@code null} being returned + * means that the cache immediately determined that it contains no mapping + * for this key. A {@link ValueWrapper} contained within the + * {@code CompletableFuture} indicates a cached value that is potentially + * {@code null}; this is sensible in a late-determined scenario where a regular + * CompletableFuture-contained {@code null} indicates a cache miss. However, + * a cache may also return a plain value if it does not support the actual + * caching of {@code null} values, avoiding the extra level of value wrapping. + * Spring's cache processing can deal with all such implementation strategies. * @since 6.1 - * @see #get(Object) + * @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"); } @@ -139,11 +147,14 @@ default CompletableFuture retrieve(Object key) { *

    If possible, implementations should ensure that the loading operation * is synchronized so that the specified {@code valueLoader} is only called * once in case of concurrent access on the same key. - *

    If the {@code valueLoader} throws an exception, it will be propagated - * to the {@code CompletableFuture} handle returned from here. + *

    Null values always indicate a user-level {@code null} value with this + * method. The provided {@link CompletableFuture} handle produces a value + * or raises an exception. If the {@code valueLoader} raises an exception, + * it will be propagated to the returned {@code CompletableFuture} handle. * @param key the key whose associated value is to be returned - * @return the value to which this cache maps the specified key, - * contained within a {@link CompletableFuture} + * @return the value to which this cache maps the specified key, contained + * within a {@link CompletableFuture} which will never be {@code null}. + * The provided future is expected to produce a value or raise an exception. * @since 6.1 * @see #retrieve(Object) * @see #get(Object, Callable) @@ -185,7 +196,7 @@ default CompletableFuture retrieve(Object key, Supplier * except that the action is performed atomically. While all out-of-the-box * {@link CacheManager} implementations are able to perform the put atomically, - * the operation may also be implemented in two steps, e.g. with a check for + * the operation may also be implemented in two steps, for example, with a check for * presence and a subsequent put, in a non-atomic way. Check the documentation * of the native cache implementation that you are using for more details. *

    The default implementation delegates to {@link #get(Object)} and @@ -199,8 +210,7 @@ default CompletableFuture retrieve(Object key, SupplierThe default implementation delegates to {@link #evict(Object)}, * returning {@code false} for not-determined prior presence of the key. * Cache providers and in particular cache decorators are encouraged - * to perform immediate eviction if possible (e.g. in case of generally + * to perform immediate eviction if possible (for example, in case of generally * deferred cache operations within a transaction) and to reliably * determine prior presence of the given key. * @param key the key whose mapping is to be removed from the cache @@ -284,8 +294,7 @@ interface ValueWrapper { /** * Return the actual value in the cache. */ - @Nullable - Object get(); + @Nullable Object get(); } @@ -297,16 +306,14 @@ interface ValueWrapper { @SuppressWarnings("serial") class ValueRetrievalException extends RuntimeException { - @Nullable - private final Object key; + private final @Nullable Object key; - public ValueRetrievalException(@Nullable Object key, Callable loader, Throwable ex) { + public ValueRetrievalException(@Nullable Object key, Callable 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 acc7242121a0..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 @@ -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,15 +19,14 @@ import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; 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; /** @@ -47,17 +46,17 @@ @SuppressWarnings("serial") public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable { - private final boolean publicMethodsOnly; - private final Set annotationParsers; + private boolean publicMethodsOnly = true; + /** * Create a default AnnotationCacheOperationSource, supporting public methods * that carry the {@code Cacheable} and {@code CacheEvict} annotations. */ public AnnotationCacheOperationSource() { - this(true); + this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser()); } /** @@ -66,10 +65,11 @@ public AnnotationCacheOperationSource() { * @param publicMethodsOnly whether to support only annotated public methods * typically for use with proxy-based AOP), or protected/private methods as well * (typically used with AspectJ class weaving) + * @see #setPublicMethodsOnly */ public AnnotationCacheOperationSource(boolean publicMethodsOnly) { + this(); this.publicMethodsOnly = publicMethodsOnly; - this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser()); } /** @@ -77,7 +77,6 @@ public AnnotationCacheOperationSource(boolean publicMethodsOnly) { * @param annotationParser the CacheAnnotationParser to use */ public AnnotationCacheOperationSource(CacheAnnotationParser annotationParser) { - this.publicMethodsOnly = true; Assert.notNull(annotationParser, "CacheAnnotationParser must not be null"); this.annotationParsers = Collections.singleton(annotationParser); } @@ -87,9 +86,8 @@ public AnnotationCacheOperationSource(CacheAnnotationParser annotationParser) { * @param annotationParsers the CacheAnnotationParser to use */ public AnnotationCacheOperationSource(CacheAnnotationParser... annotationParsers) { - this.publicMethodsOnly = true; Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified"); - this.annotationParsers = new LinkedHashSet<>(Arrays.asList(annotationParsers)); + this.annotationParsers = Set.of(annotationParsers); } /** @@ -97,12 +95,21 @@ public AnnotationCacheOperationSource(CacheAnnotationParser... annotationParsers * @param annotationParsers the CacheAnnotationParser to use */ public AnnotationCacheOperationSource(Set annotationParsers) { - this.publicMethodsOnly = true; Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified"); this.annotationParsers = annotationParsers; } + /** + * Set whether cacheable methods are expected to be public. + *

    The default is {@code true}. + * @since 6.2 + */ + public void setPublicMethodsOnly(boolean publicMethodsOnly) { + this.publicMethodsOnly = publicMethodsOnly; + } + + @Override public boolean isCandidateClass(Class targetClass) { for (CacheAnnotationParser parser : this.annotationParsers) { @@ -114,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)); } @@ -134,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); @@ -156,6 +160,7 @@ protected Collection determineCacheOperations(CacheOperationProv /** * By default, only public methods can be made cacheable. + * @see #setPublicMethodsOnly */ @Override protected boolean allowPublicMethodsOnly() { @@ -188,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/CacheConfig.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java index 234f353b142d..78da3a22e5ab 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -32,6 +32,7 @@ * @author Stephane Nicoll * @author Sam Brannen * @since 4.1 + * @see Cacheable */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -42,8 +43,10 @@ * Names of the default caches to consider for caching operations defined * in the annotated class. *

    If none is set at the operation level, these are used instead of the default. - *

    May be used to determine the target cache (or caches), matching the - * qualifier value or the bean names of a specific bean definition. + *

    Names may be used to determine the target cache(s), to be resolved via the + * configured {@link #cacheResolver()} which typically delegates to + * {@link org.springframework.cache.CacheManager#getCache}. + * For further details see {@link Cacheable#cacheNames()}. */ String[] cacheNames() default {}; diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index a207d1f06093..2d7e41468c76 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -70,8 +70,17 @@ /** * Names of the caches in which method invocation results are stored. - *

    Names may be used to determine the target cache (or caches), matching - * the qualifier value or bean name of a specific bean definition. + *

    Names may be used to determine the target cache(s), to be resolved via the + * configured {@link #cacheResolver()} which typically delegates to + * {@link org.springframework.cache.CacheManager#getCache}. + *

    This will usually be a single cache name. If multiple names are specified, + * they will be consulted for a cache hit in the order of definition, and they + * will all receive a put/evict request for the same newly cached value. + *

    Note that asynchronous/reactive cache access may not fully consult all + * specified caches, depending on the target cache. In the case of late-determined + * cache misses (for example, with Redis), further caches will not get consulted anymore. + * As a consequence, specifying multiple cache names in an async cache mode setup + * only makes sense with early-determined cache misses (for example, with Caffeine). * @since 4.2 * @see #value * @see CacheConfig#cacheNames 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 fe26f7d56d93..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 @@ -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,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 @@ -29,8 +30,12 @@ * cache management. * *

    See @{@link EnableCaching} for general examples and context; see - * {@link #cacheManager()}, {@link #cacheResolver()} and {@link #keyGenerator()} - * for detailed instructions. + * {@link #cacheManager()}, {@link #cacheResolver()}, {@link #keyGenerator()}, and + * {@link #errorHandler()} for detailed instructions. + * + *

    NOTE: A {@code CachingConfigurer} will get initialized early. + * Do not inject common dependencies into autowired fields directly; instead, consider + * declaring a lazy {@link org.springframework.beans.factory.ObjectProvider} for those. * * @author Chris Beams * @author Stephane Nicoll @@ -46,14 +51,15 @@ public interface CachingConfigurer { * management of the cache resolution, consider setting the * {@link CacheResolver} directly. *

    Implementations must explicitly declare - * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + * {@link org.springframework.context.annotation.Bean @Bean} so that + * the cache manager participates in the lifecycle of the context, for example, *

     	 * @Configuration
     	 * @EnableCaching
    -	 * public class AppConfig implements CachingConfigurer {
    +	 * class AppConfig implements CachingConfigurer {
     	 *     @Bean // important!
     	 *     @Override
    -	 *     public CacheManager cacheManager() {
    +	 *     CacheManager cacheManager() {
     	 *         // configure and return CacheManager instance
     	 *     }
     	 *     // ...
    @@ -61,8 +67,7 @@ public interface CachingConfigurer {
     	 * 
    * See @{@link EnableCaching} for more complete examples. */ - @Nullable - default CacheManager cacheManager() { + default @Nullable CacheManager cacheManager() { return null; } @@ -70,17 +75,18 @@ default CacheManager cacheManager() { * Return the {@link CacheResolver} bean to use to resolve regular caches for * annotation-driven cache management. This is an alternative and more powerful * option of specifying the {@link CacheManager} to use. - *

    If both a {@link #cacheManager()} and {@code #cacheResolver()} are set, + *

    If both a {@link #cacheManager()} and {@code cacheResolver()} are set, * the cache manager is ignored. *

    Implementations must explicitly declare - * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + * {@link org.springframework.context.annotation.Bean @Bean} so that + * the cache resolver participates in the lifecycle of the context, for example, *

     	 * @Configuration
     	 * @EnableCaching
    -	 * public class AppConfig implements CachingConfigurer {
    +	 * class AppConfig implements CachingConfigurer {
     	 *     @Bean // important!
     	 *     @Override
    -	 *     public CacheResolver cacheResolver() {
    +	 *     CacheResolver cacheResolver() {
     	 *         // configure and return CacheResolver instance
     	 *     }
     	 *     // ...
    @@ -88,56 +94,27 @@ default CacheManager cacheManager() {
     	 * 
    * See {@link EnableCaching} for more complete examples. */ - @Nullable - default CacheResolver cacheResolver() { + default @Nullable CacheResolver cacheResolver() { return null; } /** * Return the key generator bean to use for annotation-driven cache management. - * Implementations must explicitly declare - * {@link org.springframework.context.annotation.Bean @Bean}, e.g. - *
    -	 * @Configuration
    -	 * @EnableCaching
    -	 * public class AppConfig implements CachingConfigurer {
    -	 *     @Bean // important!
    -	 *     @Override
    -	 *     public KeyGenerator keyGenerator() {
    -	 *         // configure and return KeyGenerator instance
    -	 *     }
    -	 *     // ...
    -	 * }
    -	 * 
    + *

    By default, {@link org.springframework.cache.interceptor.SimpleKeyGenerator} + * is used. * See @{@link EnableCaching} for more complete examples. */ - @Nullable - default KeyGenerator keyGenerator() { + default @Nullable KeyGenerator keyGenerator() { return null; } /** * Return the {@link CacheErrorHandler} to use to handle cache-related errors. - *

    By default,{@link org.springframework.cache.interceptor.SimpleCacheErrorHandler} - * is used and simply throws the exception back at the client. - *

    Implementations must explicitly declare - * {@link org.springframework.context.annotation.Bean @Bean}, e.g. - *

    -	 * @Configuration
    -	 * @EnableCaching
    -	 * public class AppConfig implements CachingConfigurer {
    -	 *     @Bean // important!
    -	 *     @Override
    -	 *     public CacheErrorHandler errorHandler() {
    -	 *         // configure and return CacheErrorHandler instance
    -	 *     }
    -	 *     // ...
    -	 * }
    -	 * 
    + *

    By default, {@link org.springframework.cache.interceptor.SimpleCacheErrorHandler} + * 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/EnableCaching.java b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java index 06ea231402d4..1989a29f89fa 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.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. @@ -35,16 +35,16 @@ *

      * @Configuration
      * @EnableCaching
    - * public class AppConfig {
    + * class AppConfig {
      *
      *     @Bean
    - *     public MyService myService() {
    + *     MyService myService() {
      *         // configure and return a class having @Cacheable methods
      *         return new MyService();
      *     }
      *
      *     @Bean
    - *     public CacheManager cacheManager() {
    + *     CacheManager cacheManager() {
      *         // configure and return an implementation of Spring's CacheManager SPI
      *         SimpleCacheManager cacheManager = new SimpleCacheManager();
      *         cacheManager.setCaches(Set.of(new ConcurrentMapCache("default")));
    @@ -103,26 +103,25 @@
      * 
      * @Configuration
      * @EnableCaching
    - * public class AppConfig implements CachingConfigurer {
    + * class AppConfig implements CachingConfigurer {
      *
      *     @Bean
    - *     public MyService myService() {
    + *     MyService myService() {
      *         // configure and return a class having @Cacheable methods
      *         return new MyService();
      *     }
      *
      *     @Bean
      *     @Override
    - *     public CacheManager cacheManager() {
    + *     CacheManager cacheManager() {
      *         // configure and return an implementation of Spring's CacheManager SPI
      *         SimpleCacheManager cacheManager = new SimpleCacheManager();
      *         cacheManager.setCaches(Set.of(new ConcurrentMapCache("default")));
      *         return cacheManager;
      *     }
      *
    - *     @Bean
      *     @Override
    - *     public KeyGenerator keyGenerator() {
    + *     KeyGenerator keyGenerator() {
      *         // configure and return an implementation of Spring's KeyGenerator SPI
      *         return new MyKeyGenerator();
      *     }
    @@ -137,9 +136,8 @@
      * org.springframework.cache.interceptor.KeyGenerator KeyGenerator} SPI. Normally,
      * {@code @EnableCaching} will configure Spring's
      * {@link org.springframework.cache.interceptor.SimpleKeyGenerator SimpleKeyGenerator}
    - * for this purpose, but when implementing {@code CachingConfigurer}, a key generator
    - * must be provided explicitly. Return {@code null} or {@code new SimpleKeyGenerator()}
    - * from this method if no customization is necessary.
    + * for this purpose, but when implementing {@code CachingConfigurer}, a custom key
    + * generator can be specified.
      *
      * 

    {@link CachingConfigurer} offers additional customization options: * see the {@link CachingConfigurer} javadoc for further details. @@ -178,7 +176,7 @@ * For example, other beans marked with Spring's {@code @Transactional} annotation will * be upgraded to subclass proxying at the same time. This approach has no negative * impact in practice unless one is explicitly expecting one type of proxy vs another, - * e.g. in tests. + * for example, in tests. */ boolean proxyTargetClass() default false; 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 10648155c433..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,10 +155,10 @@ 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(fromStoreValue(value)) : null); + return (value != null ? CompletableFuture.completedFuture( + isAllowNullValues() ? toValueWrapper(value) : fromStoreValue(value)) : null); } @SuppressWarnings("unchecked") @@ -176,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); } @@ -222,7 +219,7 @@ protected Object toStoreValue(@Nullable Object userValue) { } @Override - 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 2d993db5ba66..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 @@ -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,12 +22,14 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; 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} @@ -35,11 +37,15 @@ * the set of cache names is pre-defined through {@link #setCacheNames}, with no * dynamic creation of further cache regions at runtime. * + *

    Supports the asynchronous {@link Cache#retrieve(Object)} and + * {@link Cache#retrieve(Object, Supplier)} operations through basic + * {@code CompletableFuture} adaptation, with early-determined cache misses. + * *

    Note: This is by no means a sophisticated CacheManager; it comes with no * cache configuration options. However, it may be useful for testing or simple * caching scenarios. For advanced local caching needs, consider - * {@link org.springframework.cache.jcache.JCacheCacheManager} or - * {@link org.springframework.cache.caffeine.CaffeineCacheManager}. + * {@link org.springframework.cache.caffeine.CaffeineCacheManager} or + * {@link org.springframework.cache.jcache.JCacheCacheManager}. * * @author Juergen Hoeller * @since 3.1 @@ -55,8 +61,7 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA private boolean storeByValue = false; - @Nullable - private SerializationDelegate serialization; + private @Nullable SerializationDelegate serialization; /** @@ -161,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); @@ -170,6 +174,15 @@ public Cache getCache(String name) { return cache; } + /** + * Remove the specified cache from this cache manager. + * @param name the name of the cache + * @since 6.1.15 + */ + public void removeCache(String name) { + this.cacheMap.remove(name); + } + private void recreateCaches() { for (Map.Entry entry : this.cacheMap.entrySet()) { entry.setValue(createConcurrentMapCache(entry.getKey())); 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 d5c71acd8a06..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. @@ -16,8 +16,13 @@ package org.springframework.cache.interceptor; +import java.util.concurrent.Callable; +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; /** @@ -26,6 +31,7 @@ * * @author Stephane Nicoll * @author Juergen Hoeller + * @author Simon Baslé * @since 4.1 * @see org.springframework.cache.interceptor.CacheErrorHandler */ @@ -67,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); } @@ -78,6 +83,69 @@ protected Cache.ValueWrapper doGet(Cache cache, Object key) { } } + /** + * Execute {@link Cache#get(Object, Callable)} on the specified + * {@link Cache} and invoke the error handler if an exception occurs. + * Invokes the {@code valueLoader} if the handler does not throw any + * exception, which simulates a cache read-through in case of error. + * @since 6.2 + * @see Cache#get(Object, Callable) + */ + protected @Nullable T doGet(Cache cache, Object key, Callable valueLoader) { + try { + return cache.get(key, valueLoader); + } + catch (Cache.ValueRetrievalException ex) { + throw ex; + } + catch (RuntimeException ex) { + getErrorHandler().handleCacheGetError(ex, cache, key); + try { + return valueLoader.call(); + } + catch (Exception ex2) { + throw new Cache.ValueRetrievalException(key, valueLoader, ex); + } + } + } + + + /** + * Execute {@link Cache#retrieve(Object)} on the specified {@link Cache} + * and invoke the error handler if an exception occurs. + * Returns {@code null} if the handler does not throw any exception, which + * simulates a cache miss in case of error. + * @since 6.2 + * @see Cache#retrieve(Object) + */ + protected @Nullable CompletableFuture doRetrieve(Cache cache, Object key) { + try { + return cache.retrieve(key); + } + 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. + * Invokes the {@code valueLoader} if the handler does not throw any + * exception, which simulates a cache read-through in case of error. + * @since 6.2 + * @see Cache#retrieve(Object, Supplier) + */ + protected CompletableFuture doRetrieve(Cache cache, Object key, Supplier> valueLoader) { + try { + return cache.retrieve(key, valueLoader); + } + catch (RuntimeException ex) { + getErrorHandler().handleCacheGetError(ex, cache, key); + return valueLoader.get(); + } + } + /** * Execute {@link Cache#put(Object, Object)} on the specified {@link Cache} * and invoke the error handler if an exception occurs. 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 d20993ae27a7..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 @@ -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. @@ -25,27 +25,25 @@ 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; /** - * Abstract implementation of {@link CacheOperation} that caches attributes + * Abstract implementation of {@link CacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. target class; 3. declaring method; 4. declaring class/interface. * - *

    Defaults to using the target class's caching attribute if none is - * associated with the target method. Any caching attribute associated with - * the target method completely overrides a class caching attribute. + *

    Defaults to using the target class's declared cache operations if none are + * associated with the target method. Any cache operations associated with + * the target method completely override any class-level declarations. * If none found on the target class, the interface that the invoked method * has been called through (in case of a JDK proxy) will be checked. * - *

    This implementation caches attributes by method after they are first - * used. If it is ever desirable to allow dynamic changing of cacheable - * attributes (which is very unlikely), caching could be made configurable. - * * @author Costin Leau * @author Juergen Hoeller * @since 3.1 @@ -53,10 +51,10 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource { /** - * Canonical value held in cache to indicate no caching attribute was - * found for this method and we don't need to look again. + * Canonical value held in cache to indicate no cache operation was + * found for this method, and we don't need to look again. */ - private static final Collection NULL_CACHING_ATTRIBUTE = Collections.emptyList(); + private static final Collection NULL_CACHING_MARKER = Collections.emptyList(); /** @@ -71,40 +69,51 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera *

    As this base class is not marked Serializable, the cache will be recreated * after serialization - provided that the concrete subclass is Serializable. */ - private final Map> attributeCache = new ConcurrentHashMap<>(1024); + private final Map> operationCache = new ConcurrentHashMap<>(1024); + @Override + public boolean hasCacheOperations(Method method, @Nullable Class targetClass) { + return !CollectionUtils.isEmpty(getCacheOperations(method, targetClass, false)); + } + + @Override + public @Nullable Collection getCacheOperations(Method method, @Nullable Class targetClass) { + return getCacheOperations(method, targetClass, true); + } + /** - * Determine the caching attribute for this method invocation. - *

    Defaults to the class's caching attribute if no method attribute is found. + * Determine the cache operations for this method invocation. + *

    Defaults to class-declared metadata if no method-level metadata is found. * @param method the method for the current invocation (never {@code null}) - * @param targetClass the target class for this invocation (may be {@code null}) + * @param targetClass the target class for this invocation (can be {@code null}) + * @param cacheNull whether {@code null} results should be cached as well * @return {@link CacheOperation} for this method, or {@code null} if the method * is not cacheable */ - @Override - @Nullable - public Collection getCacheOperations(Method method, @Nullable Class targetClass) { - if (method.getDeclaringClass() == Object.class) { + private @Nullable Collection getCacheOperations( + Method method, @Nullable Class targetClass, boolean cacheNull) { + + if (ReflectionUtils.isObjectMethod(method)) { return null; } Object cacheKey = getCacheKey(method, targetClass); - Collection cached = this.attributeCache.get(cacheKey); + Collection cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? cached : null); + return (cached != NULL_CACHING_MARKER ? cached : null); } else { Collection cacheOps = computeCacheOperations(method, targetClass); if (cacheOps != null) { if (logger.isTraceEnabled()) { - logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps); + logger.trace("Adding cacheable method '" + method.getName() + "' with operations: " + cacheOps); } - this.attributeCache.put(cacheKey, cacheOps); + this.operationCache.put(cacheKey, cacheOps); } - else { - this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + else if (cacheNull) { + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return cacheOps; } @@ -122,14 +131,13 @@ 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; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); @@ -163,22 +171,20 @@ private Collection computeCacheOperations(Method method, @Nullab /** - * Subclasses need to implement this to return the caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given class, if any. - * @param clazz the class to retrieve the attribute for - * @return all caching attribute associated with this class, or {@code null} if none + * @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 caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given method, if any. - * @param method the method to retrieve the attribute for - * @return all caching attribute associated with this method, or {@code null} if none + * @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 338dc7a1cbb5..377897db0c78 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-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,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; @@ -47,12 +49,14 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.BridgeMethodResolver; import org.springframework.core.KotlinDetector; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.SpringProperties; import org.springframework.expression.EvaluationContext; -import org.springframework.lang.Nullable; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -92,34 +96,56 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton { + /** + * System property that instructs Spring's caching infrastructure to ignore the + * presence of Reactive Streams, in particular Reactor's {@link Mono}/{@link Flux} + * in {@link org.springframework.cache.annotation.Cacheable} method return type + * declarations. + *

    By default, as of 6.1, Reactive Streams Publishers such as Reactor's + * {@link Mono}/{@link Flux} will be specifically processed for asynchronous + * caching of their produced values rather than trying to cache the returned + * {@code Publisher} instances themselves. + *

    Switch this flag to "true" in order to ignore Reactive Streams Publishers and + * process them as regular return values through synchronous caching, restoring 6.0 + * behavior. Note that this is not recommended and only works in very limited + * scenarios, for example, with manual {@code Mono.cache()}/{@code Flux.cache()} calls. + * @since 6.1.3 + * @see org.reactivestreams.Publisher + */ + public static final String IGNORE_REACTIVESTREAMS_PROPERTY_NAME = "spring.cache.reactivestreams.ignore"; + + private static final boolean shouldIgnoreReactiveStreams = + SpringProperties.getFlag(IGNORE_REACTIVESTREAMS_PROPERTY_NAME); + private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( "org.reactivestreams.Publisher", CacheAspectSupport.class.getClassLoader()); + protected final Log logger = LogFactory.getLog(getClass()); private final Map metadataCache = new ConcurrentHashMap<>(1024); - private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator(); + private final StandardEvaluationContext originalEvaluationContext = new StandardEvaluationContext(); + + 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; protected CacheAspectSupport() { - this.reactiveCachingHandler = (reactiveStreamsPresent ? new ReactiveCachingHandler() : null); + this.reactiveCachingHandler = + (reactiveStreamsPresent && !shouldIgnoreReactiveStreams ? new ReactiveCachingHandler() : null); } @@ -129,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); @@ -163,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; } @@ -199,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); } @@ -222,6 +246,7 @@ public void setCacheManager(CacheManager cacheManager) { @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; + this.originalEvaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); } @@ -240,12 +265,26 @@ public void afterSingletonsInstantiated() { setCacheManager(this.beanFactory.getBean(CacheManager.class)); } catch (NoUniqueBeanDefinitionException ex) { - throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " + - "CacheManager found. Mark one as primary or declare a specific CacheManager to use.", ex); + int numberOfBeansFound = ex.getNumberOfBeansFound(); + Collection beanNamesFound = ex.getBeanNamesFound(); + + StringBuilder message = new StringBuilder("no CacheResolver specified and expected single matching CacheManager but found "); + message.append(numberOfBeansFound); + if (beanNamesFound != null) { + message.append(": ").append(StringUtils.collectionToCommaDelimitedString(beanNamesFound)); + } + String exceptionMessage = message.toString(); + + if (beanNamesFound != null) { + throw new NoUniqueBeanDefinitionException(CacheManager.class, beanNamesFound, exceptionMessage); + } + else { + throw new NoUniqueBeanDefinitionException(CacheManager.class, numberOfBeansFound, exceptionMessage); + } } catch (NoSuchBeanDefinitionException ex) { - throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " + - "Register a CacheManager bean or remove the @EnableCaching annotation from your configuration.", ex); + throw new NoSuchBeanDefinitionException(CacheManager.class, "no CacheResolver specified - " + + "register a CacheManager bean or remove the @EnableCaching annotation from your configuration."); } } this.initialized = true; @@ -279,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); @@ -353,11 +392,10 @@ 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 = getTargetClass(target); + Class targetClass = AopProxyUtils.ultimateTargetClass(target); CacheOperationSource cacheOperationSource = getCacheOperationSource(); if (cacheOperationSource != null) { Collection operations = cacheOperationSource.getCacheOperations(method, targetClass); @@ -368,7 +406,7 @@ protected Object execute(CacheOperationInvoker invoker, Object target, Method me } } - return invoker.invoke(); + return invokeOperation(invoker); } /** @@ -381,17 +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(); } - private Class getTargetClass(Object target) { - return AopProxyUtils.ultimateTargetClass(target); - } - - @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); @@ -402,14 +434,153 @@ private Object execute(CacheOperationInvoker invoker, Method method, CacheOperat CacheOperationExpressionEvaluator.NO_RESULT); // Check if we have a cached value matching the conditions - Object cacheHit = findCachedValue(contexts.get(CacheableOperation.class)); + Object cacheHit = findCachedValue(invoker, method, contexts); + if (cacheHit == null || cacheHit instanceof Cache.ValueWrapper) { + return evaluate(cacheHit, invoker, method, contexts); + } + return cacheHit; + } + + @SuppressWarnings("unchecked") + 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())) { + 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); + if (returnValue != ReactiveCachingHandler.NOT_HANDLED) { + return returnValue; + } + } + try { + return wrapCacheValue(method, doGet(cache, key, () -> unwrapReturnValue(invokeOperation(invoker)))); + } + catch (Cache.ValueRetrievalException ex) { + // Directly propagate ThrowableWrapper from the invoker, + // or potentially also an IllegalArgumentException etc. + ReflectionUtils.rethrowRuntimeException(ex.getCause()); + // Never reached + return null; + } + } + else { + // No caching required, just call the underlying method + return invokeOperation(invoker); + } + } + + /** + * Find a cached value only for {@link CacheableOperation} that passes the condition. + * @param contexts the cacheable operations + * @return a {@link Cache.ValueWrapper} holding the cached value, + * or {@code null} if none is found + */ + 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); + Object cached = findInCaches(context, key, invoker, method, contexts); + if (cached != null) { + if (logger.isTraceEnabled()) { + logger.trace("Cache entry for key '" + key + "' found in cache(s) " + context.getCacheNames()); + } + return cached; + } + else { + if (logger.isTraceEnabled()) { + logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames()); + } + } + } + } + return null; + } + + 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.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)); + } + else { + continue; + } + } + if (this.reactiveCachingHandler != null) { + Object returnValue = this.reactiveCachingHandler.findInCaches( + context, cache, key, invoker, method, contexts); + if (returnValue != ReactiveCachingHandler.NOT_HANDLED) { + return returnValue; + } + } + Cache.ValueWrapper result = doGet(cache, key); + if (result != null) { + return result; + } + } + return null; + } + + private @Nullable Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method, + CacheOperationContexts contexts) { + + // Re-invocation in reactive pipeline after late cache hit determination? + if (contexts.processed) { + return cacheHit; + } Object cacheValue; Object returnValue; if (cacheHit != null && !hasCachePut(contexts)) { // If there are no put requests, just use the cache hit - cacheValue = (cacheHit instanceof Cache.ValueWrapper wrapper ? wrapper.get() : cacheHit); + cacheValue = unwrapCacheValue(cacheHit); returnValue = wrapCacheValue(method, cacheValue); } else { @@ -442,43 +613,17 @@ private Object execute(CacheOperationInvoker invoker, Method method, CacheOperat returnValue = returnOverride; } + // Mark as processed for re-invocation after late cache hit determination + contexts.processed = true; + return returnValue; } - @Nullable - private 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 cache.retrieve(key, () -> (CompletableFuture) invokeOperation(invoker)); - } - if (this.reactiveCachingHandler != null) { - Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key); - if (returnValue != ReactiveCachingHandler.NOT_HANDLED) { - return returnValue; - } - } - try { - return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker)))); - } - catch (Cache.ValueRetrievalException ex) { - // Directly propagate ThrowableWrapper from the invoker, - // or potentially also an IllegalArgumentException etc. - ReflectionUtils.rethrowRuntimeException(ex.getCause()); - // Never reached - return null; - } - } - else { - // No caching required, just call the underlying method - return invokeOperation(invoker); - } + 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); @@ -486,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); } @@ -509,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()) { @@ -526,7 +669,7 @@ private Object processCacheEvicts(Collection contexts, bo if (result instanceof CompletableFuture future) { return future.whenComplete((value, ex) -> { if (ex == null) { - performCacheEvicts(applicable, result); + performCacheEvicts(applicable, value); } }); } @@ -544,7 +687,7 @@ private void performCacheEvicts(List contexts, @Nullable for (CacheOperationContext context : contexts) { CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation; if (isConditionPassing(context, result)) { - Object key = null; + Object key = context.getGeneratedKey(); for (Cache cache : context.getCaches()) { if (operation.isCacheWide()) { logInvalidating(context, operation, null); @@ -570,66 +713,20 @@ private void logInvalidating(CacheOperationContext context, CacheEvictOperation } /** - * Find a cached value only for {@link CacheableOperation} that passes the condition. - * @param contexts the cacheable operations - * @return a {@link Cache.ValueWrapper} holding the cached value, - * or {@code null} if none is found - */ - @Nullable - private Object findCachedValue(Collection contexts) { - for (CacheOperationContext context : contexts) { - if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) { - Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); - Object cached = findInCaches(context, key); - if (cached != null) { - if (logger.isTraceEnabled()) { - logger.trace("Cache entry for key '" + key + "' found in cache(s) " + context.getCacheNames()); - } - return cached; - } - else { - if (logger.isTraceEnabled()) { - logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames()); - } - } - } - } - return null; - } - - /** - * Collect the {@link CachePutRequest} for all {@link CacheOperation} using - * the specified result value. + * Collect a {@link CachePutRequest} for every {@link CacheOperation} + * using the specified result value. * @param contexts the contexts to handle - * @param result the result value (never {@code null}) + * @param result the result value * @param putRequests the collection to update */ private void collectPutRequests(Collection contexts, @Nullable Object result, Collection putRequests) { for (CacheOperationContext context : contexts) { - if (isConditionPassing(context, result) && context.canPutToCache(result)) { - Object key = generateKey(context, result); - putRequests.add(new CachePutRequest(context, key)); - } - } - } - - @Nullable - private Object findInCaches(CacheOperationContext context, Object key) { - for (Cache cache : context.getCaches()) { - if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) { - return cache.retrieve(key); - } - if (this.reactiveCachingHandler != null) { - Object returnValue = this.reactiveCachingHandler.findInCaches(context, cache, key); - if (returnValue != ReactiveCachingHandler.NOT_HANDLED) { - return returnValue; - } + if (isConditionPassing(context, result)) { + putRequests.add(new CachePutRequest(context)); } - return doGet(cache, key); } - return null; } private boolean isConditionPassing(CacheOperationContext context, @Nullable Object result) { @@ -644,8 +741,10 @@ private boolean isConditionPassing(CacheOperationContext context, @Nullable Obje private Object generateKey(CacheOperationContext context, @Nullable Object result) { Object key = context.generateKey(result); if (key == null) { - throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " + - "using named params on classes without debug info?) " + context.metadata.operation); + throw new IllegalArgumentException(""" + Null key returned for cache operation [%s]. If you are using named parameters, \ + ensure that the compiler uses the '-parameters' flag.""" + .formatted(context.metadata.operation)); } if (logger.isTraceEnabled()) { logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation); @@ -660,8 +759,10 @@ private class CacheOperationContexts { private final boolean sync; + 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) { @@ -759,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; @@ -767,10 +868,11 @@ protected class CacheOperationContext implements CacheOperationInvocationContext private final Collection cacheNames; - @Nullable - private Boolean conditionPassing; + private @Nullable Boolean conditionPassing; + + 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; @@ -794,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; } @@ -841,18 +943,29 @@ 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); - return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext); + this.key = evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext); + } + else { + this.key = this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args); } - return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args); + return this.key; + } + + /** + * Get generated key. + * @return generated key + * @since 6.1.2 + */ + protected @Nullable Object getGeneratedKey() { + return this.key; } private EvaluationContext createEvaluationContext(@Nullable Object result) { return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args, - this.target, this.metadata.targetClass, this.metadata.targetMethod, result, beanFactory); + this.target, this.metadata.targetClass, this.metadata.targetMethod, result); } protected Collection getCaches() { @@ -916,22 +1029,15 @@ private class CachePutRequest { private final CacheOperationContext context; - private final Object key; - - public CachePutRequest(CacheOperationContext context, Object key) { + public CachePutRequest(CacheOperationContext context) { this.context = context; - this.key = key; } - @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) { - performEvict(ex); - } - else { - performPut(value); + if (ex == null) { + performCachePut(value); } }); } @@ -941,36 +1047,33 @@ public Object apply(@Nullable Object result) { return returnValue; } } - performPut(result); + performCachePut(result); return null; } - void performPut(@Nullable Object value) { - if (logger.isTraceEnabled()) { - logger.trace("Creating cache entry for key '" + this.key + "' in cache(s) " + - this.context.getCacheNames()); - } - for (Cache cache : this.context.getCaches()) { - doPut(cache, this.key, value); - } - } - - void performEvict(Throwable cause) { - if (logger.isTraceEnabled()) { - logger.trace("Removing cache entry for key '" + this.key + "' from cache(s) " + - this.context.getCacheNames() + " due to exception: " + cause); - } - for (Cache cache : this.context.getCaches()) { - doEvict(cache, this.key, false); + public void performCachePut(@Nullable Object value) { + if (this.context.canPutToCache(value)) { + Object key = this.context.getGeneratedKey(); + if (key == null) { + key = generateKey(this.context, value); + } + if (logger.isTraceEnabled()) { + logger.trace("Creating cache entry for key '" + key + "' in cache(s) " + + this.context.getCacheNames()); + } + for (Cache cache : this.context.getCaches()) { + doPut(cache, key, value); + } } } } /** - * Reactive Streams Subscriber collection for collecting a List to cache. + * Reactive Streams Subscriber for exhausting the Flux and collecting a List + * to cache. */ - private class CachePutListSubscriber implements Subscriber { + private final class CachePutListSubscriber implements Subscriber { private final CachePutRequest request; @@ -990,11 +1093,11 @@ public void onNext(Object o) { } @Override public void onError(Throwable t) { - this.request.performEvict(t); + this.cacheValue.clear(); } @Override public void onComplete() { - this.request.performPut(this.cacheValue); + this.request.performCachePut(this.cacheValue); } } @@ -1008,71 +1111,146 @@ 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) invokeOperation(invoker)).toFuture())); + 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)) - .doOnSuccess(value -> performCacheEvicts(contexts, result))); + .doOnSuccess(value -> performCacheEvicts(contexts, value))); } return NOT_HANDLED; } - @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()); if (adapter != null) { - CompletableFuture cachedFuture = cache.retrieve(key); + CompletableFuture cachedFuture = doRetrieve(cache, key); if (cachedFuture == null) { return null; } if (adapter.isMultiValue()) { return adapter.fromPublisher(Flux.from(Mono.fromFuture(cachedFuture)) - .flatMap(v -> (v instanceof Iterable iv ? Flux.fromIterable(iv) : Flux.just(v)))); + .switchIfEmpty(Flux.defer(() -> (Flux) evaluate(null, invoker, method, contexts))) + .flatMap(v -> evaluate(valueToFlux(v, contexts), invoker, method, contexts)) + .onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); + Object e = evaluate(null, invoker, method, contexts); + return (e != null ? e : Flux.error((RuntimeException) ex)); + } + catch (RuntimeException exception) { + return Flux.error(exception); + } + })); } else { - return adapter.fromPublisher(Mono.fromFuture(cachedFuture)); + return adapter.fromPublisher(Mono.fromFuture(cachedFuture) + .switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts))) + .flatMap(v -> evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts)) + .onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); + Object e = evaluate(null, invoker, method, contexts); + return (e != null ? e : Mono.error((RuntimeException) ex)); + } + catch (RuntimeException exception) { + return Mono.error(exception); + } + })); } } return NOT_HANDLED; } - @Nullable - public Object processPutRequest(CachePutRequest request, @Nullable Object result) { + private Flux valueToFlux(Object value, CacheOperationContexts contexts) { + Object data = unwrapCacheValue(value); + return (!contexts.processed && data instanceof Iterable iterable ? Flux.fromIterable(iterable) : + (data != null ? Flux.just(data) : Flux.empty())); + } + + 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()) { - Flux source = Flux.from(adapter.toPublisher(result)); + Flux source = Flux.from(adapter.toPublisher(result)) + .publish().refCount(2); source.subscribe(new CachePutListSubscriber(request)); return adapter.fromPublisher(source); } else { return adapter.fromPublisher(Mono.from(adapter.toPublisher(result)) - .doOnSuccess(request::performPut).doOnError(request::performEvict)); + .doOnSuccess(request::performCachePut)); } } return NOT_HANDLED; 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 25d4282313e1..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,17 +20,18 @@ 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 a method parameters as SpEL - * variables, in a lazy manner. The lazy nature eliminates unneeded - * parsing of classes byte code for parameter discovery. + * Cache-specific evaluation context that adds method parameters as SpEL + * variables, in a lazy manner. The lazy nature avoids unnecessary + * parsing of a class's byte code for parameter discovery. * - *

    Also define a set of "unavailable variables" (i.e. variables that should - * lead to an exception right the way when they are accessed). This can be useful + *

    Also defines a set of "unavailable variables" (i.e. variables that should + * lead to an exception as soon as they are accessed). This can be useful * to verify a condition does not match even when not all potential variables * are present. * @@ -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); @@ -55,10 +56,10 @@ class CacheEvaluationContext extends MethodBasedEvaluationContext { /** - * Add the specified variable name as unavailable for that context. - * Any expression trying to access this variable should lead to an exception. - *

    This permits the validation of expressions that could potentially a - * variable even when such variable isn't available yet. Any expression + * Add the specified variable name as unavailable for this context. + *

    Any expression trying to access this variable should lead to an exception. + *

    This permits the validation of expressions that could potentially access + * a variable even when such a variable isn't available yet. Any expression * trying to use that variable should therefore fail to evaluate. */ public void addUnavailableVariable(String name) { @@ -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 new file mode 100644 index 000000000000..d417f4b80be6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java @@ -0,0 +1,73 @@ +/* + * 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.cache.interceptor; + +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.util.function.SingletonSupplier; + +/** + * A factory for {@link CacheEvaluationContext} that makes sure that internal + * delegates are reused. + * + * @author Stephane Nicoll + * @since 6.1.1 + */ +class CacheEvaluationContextFactory { + + private final StandardEvaluationContext originalContext; + + private @Nullable Supplier parameterNameDiscoverer; + + CacheEvaluationContextFactory(StandardEvaluationContext originalContext) { + this.originalContext = originalContext; + } + + public void setParameterNameDiscoverer(Supplier parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + public ParameterNameDiscoverer getParameterNameDiscoverer() { + if (this.parameterNameDiscoverer == null) { + this.parameterNameDiscoverer = SingletonSupplier.of(new DefaultParameterNameDiscoverer()); + } + return this.parameterNameDiscoverer.get(); + } + + /** + * Creates a {@link CacheEvaluationContext} for the specified operation. + * @param rootObject the {@code root} object to use for the context + * @param targetMethod the target cache {@link Method} + * @param args the arguments of the method invocation + * @return a context suitable for this cache operation + */ + public CacheEvaluationContext forOperation(CacheExpressionRootObject rootObject, + Method targetMethod, @Nullable Object[] args) { + + CacheEvaluationContext evaluationContext = new CacheEvaluationContext( + rootObject, targetMethod, args, getParameterNameDiscoverer()); + this.originalContext.applyDelegatesTo(evaluationContext); + return evaluationContext; + } + +} 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 a0975f05aa17..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 @@ -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,16 +19,10 @@ import java.io.Serializable; import java.lang.reflect.Method; -import kotlin.coroutines.Continuation; -import kotlin.coroutines.CoroutineContext; -import kotlinx.coroutines.Job; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.reactivestreams.Publisher; +import org.jspecify.annotations.Nullable; -import org.springframework.core.CoroutinesUtils; -import org.springframework.core.KotlinDetector; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -52,15 +46,11 @@ 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 = () -> { try { - if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) { - return KotlinDelegate.invokeSuspendingFunction(method, invocation.getThis(), invocation.getArguments()); - } return invocation.proceed(); } catch (Throwable ex) { @@ -78,16 +68,4 @@ public Object invoke(final MethodInvocation invocation) throws Throwable { } } - /** - * Inner class to avoid a hard dependency on Kotlin at runtime. - */ - private static class KotlinDelegate { - - public static Publisher invokeSuspendingFunction(Method method, Object target, Object... args) { - Continuation continuation = (Continuation) args[args.length - 1]; - CoroutineContext coroutineContext = continuation.getContext().minusKey(Job.Key); - return CoroutinesUtils.invokeSuspendingFunction(coroutineContext, method, target, args); - } - } - } 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 aaee9b396b75..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 @@ -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. @@ -17,11 +17,12 @@ package org.springframework.cache.interceptor; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Base class for cache operations. @@ -158,7 +159,7 @@ public void setCacheName(String cacheName) { } public void setCacheNames(String... cacheNames) { - this.cacheNames = new LinkedHashSet<>(cacheNames.length); + this.cacheNames = CollectionUtils.newLinkedHashSet(cacheNames.length); for (String cacheName : cacheNames) { Assert.hasText(cacheName, "Cache name must be non-empty if specified"); this.cacheNames.add(cacheName); 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 82892d0ccfb2..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -21,14 +21,13 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.beans.factory.BeanFactory; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; import org.springframework.context.expression.AnnotatedElementKey; -import org.springframework.context.expression.BeanFactoryResolver; 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. @@ -67,6 +66,13 @@ class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator { private final Map unlessCache = new ConcurrentHashMap<>(64); + private final CacheEvaluationContextFactory evaluationContextFactory; + + public CacheOperationExpressionEvaluator(CacheEvaluationContextFactory evaluationContextFactory) { + super(); + this.evaluationContextFactory = evaluationContextFactory; + this.evaluationContextFactory.setParameterNameDiscoverer(this::getParameterNameDiscoverer); + } /** * Create an {@link EvaluationContext}. @@ -80,27 +86,23 @@ class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator { * @return the evaluation context */ public EvaluationContext createEvaluationContext(Collection caches, - Method method, Object[] args, Object target, Class targetClass, Method targetMethod, - @Nullable Object result, @Nullable BeanFactory beanFactory) { + Method method, @Nullable Object[] args, Object target, Class targetClass, Method targetMethod, + @Nullable Object result) { CacheExpressionRootObject rootObject = new CacheExpressionRootObject( caches, method, args, target, targetClass); - CacheEvaluationContext evaluationContext = new CacheEvaluationContext( - rootObject, targetMethod, args, getParameterNameDiscoverer()); + CacheEvaluationContext evaluationContext = this.evaluationContextFactory + .forOperation(rootObject, targetMethod, args); if (result == RESULT_UNAVAILABLE) { evaluationContext.addUnavailableVariable(RESULT_VARIABLE); } else if (result != NO_RESULT) { evaluationContext.setVariable(RESULT_VARIABLE, result); } - if (beanFactory != null) { - evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); - } 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 cfaab08137bd..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -16,13 +16,13 @@ package org.springframework.cache.interceptor; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract the invocation of a cache operation. * *

    Does not provide a way to transmit checked exceptions but - * provide a special exception that should be used to wrap any + * provides a special exception that should be used to wrap any * exception that was thrown by the underlying invocation. * Callers are expected to handle this issue type specifically. * @@ -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 02a9b4f41646..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,7 +19,9 @@ import java.lang.reflect.Method; import java.util.Collection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.util.CollectionUtils; /** * Interface used by {@link CacheInterceptor}. Implementations know how to source @@ -45,20 +47,32 @@ public interface CacheOperationSource { * metadata at class or method level; {@code true} otherwise. The default * implementation returns {@code true}, leading to regular introspection. * @since 5.2 + * @see #hasCacheOperations */ default boolean isCandidateClass(Class targetClass) { return true; } + /** + * Determine whether there are cache operations for the given method. + * @param method the method to introspect + * @param targetClass the target class (can be {@code null}, + * in which case the declaring class of the method must be used) + * @since 6.2 + * @see #getCacheOperations + */ + default boolean hasCacheOperations(Method method, @Nullable Class targetClass) { + return !CollectionUtils.isEmpty(getCacheOperations(method, targetClass)); + } + /** * Return the collection of cache operations for this method, * or {@code null} if the method contains no cacheable annotations. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * 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 e70275aeaed7..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 @@ -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,16 +19,16 @@ 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.CollectionUtils; import org.springframework.util.ObjectUtils; /** * A {@code Pointcut} that matches if the underlying {@link CacheOperationSource} - * has an attribute for a given method. + * has an operation for a given method. * * @author Costin Leau * @author Juergen Hoeller @@ -36,10 +36,9 @@ * @since 3.1 */ @SuppressWarnings("serial") -class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { +final class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { - @Nullable - private CacheOperationSource cacheOperationSource; + private @Nullable CacheOperationSource cacheOperationSource; public CacheOperationSourcePointcut() { @@ -54,7 +53,7 @@ public void setCacheOperationSource(@Nullable CacheOperationSource cacheOperatio @Override public boolean matches(Method method, Class targetClass) { return (this.cacheOperationSource == null || - !CollectionUtils.isEmpty(this.cacheOperationSource.getCacheOperations(method, targetClass))); + this.cacheOperationSource.hasCacheOperations(method, targetClass)); } @Override @@ -78,7 +77,7 @@ public String toString() { * {@link ClassFilter} that delegates to {@link CacheOperationSource#isCandidateClass} * for filtering classes whose methods are not worth searching to begin with. */ - private class CacheOperationSourceClassFilter implements ClassFilter { + private final class CacheOperationSourceClassFilter implements ClassFilter { @Override public boolean matches(Class clazz) { @@ -88,14 +87,14 @@ public boolean matches(Class clazz) { return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); } - private CacheOperationSource getCacheOperationSource() { + private @Nullable CacheOperationSource getCacheOperationSource() { return cacheOperationSource; } @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof CacheOperationSourceClassFilter that && - ObjectUtils.nullSafeEquals(cacheOperationSource, that.getCacheOperationSource()))); + ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); } @Override @@ -105,9 +104,8 @@ public int hashCode() { @Override public String toString() { - return CacheOperationSourceClassFilter.class.getName() + ": " + cacheOperationSource; + return CacheOperationSourceClassFilter.class.getName() + ": " + getCacheOperationSource(); } - } } 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 da9533717ad3..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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,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; /** @@ -67,8 +68,17 @@ public boolean isCandidateClass(Class targetClass) { } @Override - @Nullable - public Collection getCacheOperations(Method method, @Nullable Class targetClass) { + public boolean hasCacheOperations(Method method, @Nullable Class targetClass) { + for (CacheOperationSource source : this.cacheOperationSources) { + if (source.hasCacheOperations(method, targetClass)) { + return true; + } + } + return false; + } + + @Override + 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 a06cb8cf4bed..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; @@ -52,7 +52,7 @@ public class NameMatchCacheOperationSource implements CacheOperationSource, Seri /** * Set a name/attribute map, consisting of method names - * (e.g. "myMethod") and CacheOperation instances + * (for example, "myMethod") and CacheOperation instances * (or Strings to be converted to CacheOperation instances). * @see CacheOperation */ @@ -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 a8023cea59cb..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,7 +52,7 @@ public void setCacheNames(Collection cacheNames) { } @Override - 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/VariableNotAvailableException.java b/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java index b4ce3c2b0e2c..48a30ae1f8bd 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,8 +19,8 @@ import org.springframework.expression.EvaluationException; /** - * A specific {@link EvaluationException} to mention that a given variable - * used in the expression is not available in the context. + * An internal {@link EvaluationException} which signals that a given variable + * used in an expression is not available in the context. * * @author Stephane Nicoll * @since 4.0.6 @@ -28,17 +28,8 @@ @SuppressWarnings("serial") class VariableNotAvailableException extends EvaluationException { - private final String name; - - public VariableNotAvailableException(String name) { - super("Variable not available"); - this.name = name; - } - - - public final String getName() { - return this.name; + super("Variable '" + name + "' not available"); } } 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 c7f7ef5da0aa..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 @@ -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,12 @@ 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; /** * Abstract base class implementing the common {@link CacheManager} methods. @@ -64,7 +66,7 @@ public void initializeCaches() { synchronized (this.cacheMap) { this.cacheNames = Collections.emptySet(); this.cacheMap.clear(); - Set cacheNames = new LinkedHashSet<>(caches.size()); + Set cacheNames = CollectionUtils.newLinkedHashSet(caches.size()); for (Cache cache : caches) { String name = cache.getName(); this.cacheMap.put(name, decorateCache(cache)); @@ -85,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) { @@ -127,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); } @@ -171,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 700936a85a89..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -16,8 +16,11 @@ package org.springframework.cache.support; +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}, @@ -28,8 +31,7 @@ */ public class SimpleValueWrapper implements ValueWrapper { - @Nullable - private final Object value; + private final @Nullable Object value; /** @@ -45,9 +47,23 @@ 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; } + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ValueWrapper wrapper && Objects.equals(get(), wrapper.get()))); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.value); + } + + @Override + public String toString() { + return "ValueWrapper for [" + 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 35c667cec610..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. @@ -107,7 +106,7 @@ public interface ApplicationContext extends EnvironmentCapable, ListableBeanFact * @return the AutowireCapableBeanFactory for this context * @throws IllegalStateException if the context does not support the * {@link AutowireCapableBeanFactory} interface, or does not hold an - * autowire-capable bean factory yet (e.g. if {@code refresh()} has + * autowire-capable bean factory yet (for example, if {@code refresh()} has * never been called), or if the context has been closed already * @see ConfigurableApplicationContext#refresh() * @see ConfigurableApplicationContext#getBeanFactory() diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationContextInitializer.java b/spring-context/src/main/java/org/springframework/context/ApplicationContextInitializer.java index 42e750fc2476..107793410ea1 100644 --- a/spring-context/src/main/java/org/springframework/context/ApplicationContextInitializer.java +++ b/spring-context/src/main/java/org/springframework/context/ApplicationContextInitializer.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. @@ -44,7 +44,7 @@ public interface ApplicationContextInitializerFor 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 662fdb2bfca1..d33f146834ab 100644 --- a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.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. @@ -17,6 +17,9 @@ package org.springframework.context; 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; @@ -25,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. @@ -53,6 +55,16 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life */ String CONFIG_LOCATION_DELIMITERS = ",; \t\n"; + /** + * The name of the {@link Executor bootstrap executor} bean in the context. + * If none is supplied, no background bootstrapping will be active. + * @since 6.2 + * @see java.util.concurrent.Executor + * @see org.springframework.core.task.TaskExecutor + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setBootstrapExecutor + */ + String BOOTSTRAP_EXECUTOR_BEAN_NAME = "bootstrapExecutor"; + /** * Name of the ConversionService bean in the factory. * If none is supplied, default conversion rules apply. @@ -213,8 +225,8 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life * on JVM shutdown unless it has already been closed at that time. *

    This method can be called multiple times. Only one shutdown hook * (at max) will be registered for each context instance. - *

    As of Spring Framework 5.2, the {@linkplain Thread#getName() name} of - * the shutdown hook thread should be {@link #SHUTDOWN_HOOK_THREAD_NAME}. + *

    The {@linkplain Thread#getName() name} of the shutdown hook thread + * should be {@link #SHUTDOWN_HOOK_THREAD_NAME}. * @see java.lang.Runtime#addShutdownHook * @see #close() */ @@ -231,6 +243,18 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life @Override void close(); + /** + * Return whether this context has been closed already, that is, + * whether {@link #close()} has been called on an active context + * in order to initiate its shutdown. + *

    Note: This does not indicate whether context shutdown has completed. + * Use {@link #isActive()} for differentiating between those scenarios: + * a context becomes inactive once it has been fully shut down and the + * original {@code close()} call has returned. + * @since 6.2 + */ + boolean isClosed(); + /** * Determine whether this application context is active, that is, * whether it has been refreshed at least once and has not been closed yet. 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/Lifecycle.java b/spring-context/src/main/java/org/springframework/context/Lifecycle.java index 99ba5211ee36..9f7a7c0fa1bd 100644 --- a/spring-context/src/main/java/org/springframework/context/Lifecycle.java +++ b/spring-context/src/main/java/org/springframework/context/Lifecycle.java @@ -25,7 +25,7 @@ *

    Can be implemented by both components (typically a Spring bean defined in a * Spring context) and containers (typically a Spring {@link ApplicationContext} * itself). Containers will propagate start/stop signals to all components that - * apply within each container, e.g. for a stop/restart scenario at runtime. + * apply within each container, for example, for a stop/restart scenario at runtime. * *

    Can be used for direct invocations or for management operations via JMX. * In the latter case, the {@link org.springframework.jmx.export.MBeanExporter} diff --git a/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java index 6be0e0e5996d..a96a63753420 100644 --- a/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java @@ -26,12 +26,12 @@ public interface LifecycleProcessor extends Lifecycle { /** - * Notification of context refresh, e.g. for auto-starting components. + * Notification of context refresh, for example, for auto-starting components. */ void onRefresh(); /** - * Notification of context close phase, e.g. for auto-stopping components. + * Notification of context close phase, for example, for auto-stopping components. */ void onClose(); 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 54f5ea88ed05..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 @@ -41,7 +41,7 @@ public interface MessageSource { /** * Try to resolve the message. Return default message if no message was found. - * @param code the message code to look up, e.g. 'calculator.noRateSet'. + * @param code the message code to look up, for example, 'calculator.noRateSet'. * MessageSource users are encouraged to base message names on qualified class * or package names, avoiding potential conflicts and ensuring maximum clarity. * @param args an array of arguments that will be filled in for params within @@ -54,12 +54,11 @@ 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. - * @param code the message code to look up, e.g. 'calculator.noRateSet'. + * @param code the message code to look up, for example, 'calculator.noRateSet'. * MessageSource users are encouraged to base message names on qualified class * or package names, avoiding potential conflicts and ensuring maximum clarity. * @param args an array of arguments that will be filled in for params within @@ -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 5f3f0a6c959a..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; /** @@ -56,7 +57,7 @@ public PayloadApplicationEvent(Object source, T payload) { * @param source the object on which the event initially occurred (never {@code null}) * @param payload the payload object (never {@code null}) * @param payloadType the type object of payload object (can be {@code null}). - * Note that this is meant to indicate the payload type (e.g. {@code String}), + * Note that this is meant to indicate the payload type (for example, {@code String}), * not the full event type (such as {@code PayloadApplicationEvent<<String>}). * @since 6.0 */ diff --git a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java index 06f98a4048ca..204f75e61477 100644 --- a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java +++ b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -72,9 +72,12 @@ public interface SmartLifecycle extends Lifecycle, Phased { * {@link Lifecycle} implementations, putting the typically auto-started * {@code SmartLifecycle} beans into a later startup phase and an earlier * shutdown phase. + *

    Note that certain {@code SmartLifecycle} components come with a different + * default phase: for example, executors/schedulers with {@code Integer.MAX_VALUE / 2}. * @since 5.1 * @see #getPhase() - * @see org.springframework.context.support.DefaultLifecycleProcessor#getPhase(Lifecycle) + * @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#DEFAULT_PHASE + * @see org.springframework.context.support.DefaultLifecycleProcessor#setTimeoutPerShutdownPhase */ int DEFAULT_PHASE = Integer.MAX_VALUE; @@ -121,7 +124,7 @@ default void stop(Runnable callback) { /** * Return the phase that this lifecycle object is supposed to run in. *

    The default implementation returns {@link #DEFAULT_PHASE} in order to - * let {@code stop()} callbacks execute after regular {@code Lifecycle} + * let {@code stop()} callbacks execute before regular {@code Lifecycle} * implementations. * @see #isAutoStartup() * @see #start() 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 286059addd2e..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 @@ -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.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; /** @@ -43,6 +44,7 @@ * @author Chris Beams * @author Sam Brannen * @author Phillip Webb + * @author Yanming Zhou * @since 3.0 * @see AnnotationConfigApplicationContext#register */ @@ -59,7 +61,7 @@ public class AnnotatedBeanDefinitionReader { /** * Create a new {@code AnnotatedBeanDefinitionReader} for the given registry. - *

    If the registry is {@link EnvironmentCapable}, e.g. is an {@code ApplicationContext}, + *

    If the registry is {@link EnvironmentCapable}, for example, is an {@code ApplicationContext}, * the {@link Environment} will be inherited, otherwise a new * {@link StandardEnvironment} will be created and used. * @param registry the {@code BeanFactory} to load bean definitions into, @@ -100,7 +102,6 @@ public final BeanDefinitionRegistry getRegistry() { * Set the {@code Environment} to use when evaluating whether * {@link Conditional @Conditional}-annotated component classes should be registered. *

    The default is a {@link StandardEnvironment}. - * @see #registerBean(Class, String, Class...) */ public void setEnvironment(Environment environment) { this.conditionEvaluator = new ConditionEvaluator(this.registry, environment, null); @@ -130,7 +131,7 @@ public void setScopeMetadataResolver(@Nullable ScopeMetadataResolver scopeMetada *

    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, - * e.g. {@link Configuration @Configuration} classes + * for example, {@link Configuration @Configuration} classes */ public void register(Class... componentClasses) { for (Class componentClass : componentClasses) { @@ -224,7 +225,7 @@ public void registerBean(Class beanClass, @Nullable String name, @Nullabl * @param supplier a callback for creating an instance of the bean * (may be {@code null}) * @param customizers one or more callbacks for customizing the factory's - * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * {@link BeanDefinition}, for example, setting a lazy-init or primary flag * @since 5.2 */ public void registerBean(Class beanClass, @Nullable String name, @Nullable Supplier supplier, @@ -243,12 +244,12 @@ public void registerBean(Class beanClass, @Nullable String name, @Nullabl * @param supplier a callback for creating an instance of the bean * (may be {@code null}) * @param customizers one or more callbacks for customizing the factory's - * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * {@link BeanDefinition}, for example, setting a lazy-init or primary flag * @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())) { @@ -267,6 +268,9 @@ private void doRegisterBean(Class beanClass, @Nullable String name, if (Primary.class == qualifier) { abd.setPrimary(true); } + else if (Fallback.class == qualifier) { + abd.setFallback(true); + } else if (Lazy.class == qualifier) { abd.setLazyInit(true); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java index 4f0f8a7e62b5..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.context.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -28,19 +29,21 @@ 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; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation.Adapt; 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; import org.springframework.util.StringUtils; /** @@ -51,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 @@ -122,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); @@ -147,12 +146,13 @@ protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotat Set metaAnnotationTypes = this.metaAnnotationTypesCache.computeIfAbsent(annotationType, key -> getMetaAnnotationTypes(mergedAnnotation)); if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) { - Object value = attributes.get("value"); - if (value instanceof String currentName && !currentName.isBlank()) { + Object value = attributes.get(MergedAnnotation.VALUE); + if (value instanceof String currentName && !currentName.isBlank() && + !hasExplicitlyAliasedValueAttribute(mergedAnnotation.getType())) { if (conventionBasedStereotypeCheckCache.add(annotationType) && metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) { logger.warn(""" - Support for convention-based stereotype names is deprecated and will \ + 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.""" @@ -188,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) @@ -216,15 +215,12 @@ 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("value")); + return (isStereotype && attributes.containsKey(MergedAnnotation.VALUE)); } /** @@ -241,7 +237,7 @@ protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionR /** * Derive a default bean name from the given bean definition. *

    The default implementation simply builds a decapitalized version - * of the short class name: e.g. "mypackage.MyJdbcDao" → "myJdbcDao". + * of the short class name: for example, "mypackage.MyJdbcDao" → "myJdbcDao". *

    Note that inner classes will thus have names of the form * "outerClassName.InnerClassName", which because of the period in the * name may be an issue if you are autowiring by name. @@ -255,4 +251,14 @@ protected String buildDefaultBeanName(BeanDefinition definition) { return StringUtils.uncapitalizeAsProperty(shortClassName); } + /** + * Determine if the supplied annotation type declares a {@code value()} attribute + * with an explicit alias configured via {@link AliasFor @AliasFor}. + * @since 6.2.3 + */ + private static boolean hasExplicitlyAliasedValueAttribute(Class annotationType) { + Method valueAttribute = ReflectionUtils.findMethod(annotationType, MergedAnnotation.VALUE); + return (valueAttribute != null && valueAttribute.isAnnotationPresent(AliasFor.class)); + } + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/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 45355379340d..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, - * e.g. {@link Configuration @Configuration} 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 0a27be3529b4..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 @@ -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. @@ -17,8 +17,10 @@ package org.springframework.context.annotation; import java.lang.annotation.Annotation; -import java.util.LinkedHashSet; 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; @@ -32,10 +34,11 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +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; /** * Utility class that allows for convenient registration of common @@ -115,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); @@ -152,7 +152,7 @@ public static Set registerAnnotationConfigProcessors( } } - Set beanDefs = new LinkedHashSet<>(8); + Set beanDefs = CollectionUtils.newLinkedHashSet(6); if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class); @@ -167,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)); @@ -212,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; } @@ -244,6 +242,9 @@ else if (abd.getMetadata() != metadata) { if (metadata.isAnnotated(Primary.class.getName())) { abd.setPrimary(true); } + if (metadata.isAnnotated(Fallback.class.getName())) { + abd.setFallback(true); + } AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class); if (dependsOn != null) { abd.setDependsOn(dependsOn.getStringArray("value")); @@ -270,20 +271,19 @@ 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)); } static Set attributesForRepeatable(AnnotationMetadata metadata, - Class annotationType, Class containerType) { + Class annotationType, Class containerType, + Predicate> predicate) { - return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false); + return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, predicate, false, false); } static Set attributesForRepeatable(AnnotationMetadata metadata, diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java index bbc64c4b359a..7f068fef659e 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Bean.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. @@ -239,13 +239,41 @@ String[] name() default {}; /** - * Is this bean a candidate for getting autowired into some other bean? + * Is this bean a candidate for getting autowired into some other bean at all? *

    Default is {@code true}; set this to {@code false} for internal delegates * that are not meant to get in the way of beans of the same type in other places. * @since 5.1 + * @see #defaultCandidate() */ boolean autowireCandidate() default true; + /** + * Is this bean a candidate for getting autowired into some other bean based on + * the plain type, without any further indications such as a qualifier match? + *

    Default is {@code true}; set this to {@code false} for restricted delegates + * that are supposed to be injectable in certain areas but are not meant to get + * in the way of beans of the same type in other places. + *

    This is a variation of {@link #autowireCandidate()} which does not disable + * injection in general, just enforces an additional indication such as a qualifier. + * @since 6.2 + * @see #autowireCandidate() + */ + boolean defaultCandidate() default true; + + /** + * The bootstrap mode for this bean: default is the main pre-instantiation thread + * for non-lazy singleton beans and the caller thread for prototype beans. + *

    Set {@link Bootstrap#BACKGROUND} to allow for instantiating this bean on a + * background thread. For a non-lazy singleton, a background pre-instantiation + * thread can be used then, while still enforcing the completion at the end of + * {@link org.springframework.context.ConfigurableApplicationContext#refresh()}. + * For a lazy singleton, a background pre-instantiation thread can be used as well + * - with completion allowed at a later point, enforcing it when actually accessed. + * @since 6.2 + * @see Lazy + */ + Bootstrap bootstrap() default Bootstrap.DEFAULT; + /** * The optional name of a method to call on the bean instance during initialization. * Not commonly used, given that the method may be called programmatically directly @@ -272,7 +300,7 @@ * method (i.e., detection occurs reflectively against the bean instance itself at * creation time). *

    To disable destroy method inference for a particular {@code @Bean}, specify an - * empty string as the value, e.g. {@code @Bean(destroyMethod="")}. Note that the + * empty string as the value, for example, {@code @Bean(destroyMethod="")}. Note that the * {@link org.springframework.beans.factory.DisposableBean} callback interface will * nevertheless get detected and the corresponding destroy method invoked: In other * words, {@code destroyMethod=""} only affects custom close/shutdown methods and @@ -285,4 +313,28 @@ */ String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; + + /** + * Local enumeration for the bootstrap mode. + * @since 6.2 + * @see #bootstrap() + */ + enum Bootstrap { + + /** + * Constant to indicate the main pre-instantiation thread for non-lazy + * singleton beans and the caller thread for prototype beans. + */ + DEFAULT, + + /** + * Allow for instantiating a bean on a background thread. + *

    For a non-lazy singleton, a background pre-instantiation thread + * can be used while still enforcing the completion on context refresh. + * For a lazy singleton, a background pre-instantiation thread can be used + * with completion allowed at a later point (when actually accessed). + */ + BACKGROUND, + } + } 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 ea09841aee31..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 @@ -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,10 +16,14 @@ package org.springframework.context.annotation; +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 @@ -41,7 +45,14 @@ final class BeanMethod extends ConfigurationMethod { @Override + @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 + // in any case whereas @Autowired methods are setter-like methods on the containing class + problemReporter.error(new AutowiredDeclaredMethodError()); + } + if ("void".equals(getMetadata().getReturnTypeName())) { // declared as void: potential misuse of @Bean, maybe meant as init method instead? problemReporter.error(new VoidDeclaredMethodError()); @@ -52,22 +63,24 @@ public void validate(ProblemReporter problemReporter) { return; } - if (this.configurationClass.getMetadata().isAnnotated(Configuration.class.getName())) { - if (!getMetadata().isOverridable()) { - // instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB - problemReporter.error(new NonOverridableMethodError()); - } + 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 + problemReporter.error(new NonOverridableMethodError()); } } @Override public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof BeanMethod that && this.metadata.equals(that.metadata))); + return (this == other || (other instanceof BeanMethod that && + this.configurationClass.equals(that.configurationClass) && + getLocalMethodIdentifier(this.metadata).equals(getLocalMethodIdentifier(that.metadata)))); } @Override public int hashCode() { - return this.metadata.hashCode(); + return this.configurationClass.hashCode() * 31 + getLocalMethodIdentifier(this.metadata).hashCode(); } @Override @@ -76,6 +89,23 @@ public String toString() { } + private static String getLocalMethodIdentifier(MethodMetadata metadata) { + String metadataString = metadata.toString(); + int index = metadataString.indexOf(metadata.getDeclaringClassName()); + return (index >= 0 ? metadataString.substring(index + metadata.getDeclaringClassName().length()) : + metadataString); + } + + + private class AutowiredDeclaredMethodError extends Problem { + + AutowiredDeclaredMethodError() { + super("@Bean method '%s' must not be declared as autowired; remove the method-level @Autowired annotation." + .formatted(getMetadata().getMethodName()), getResourceLocation()); + } + } + + private class VoidDeclaredMethodError extends Problem { VoidDeclaredMethodError() { 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 7aa22b2eb7ad..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; } @@ -312,7 +311,7 @@ protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, /** * Register the specified bean with the given registry. - *

    Can be overridden in subclasses, e.g. to adapt the registration + *

    Can be overridden in subclasses, for example, to adapt the registration * process or to register further bean definitions for each scanned bean. * @param definitionHolder the bean definition plus bean name for the bean * @param registry the BeanDefinitionRegistry to register the bean with 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 1490284a4e01..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.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; @@ -36,6 +37,7 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.context.index.CandidateComponentsIndex; import org.springframework.context.index.CandidateComponentsIndexLoader; +import org.springframework.core.SpringProperties; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; @@ -47,12 +49,12 @@ import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.ClassFormatException; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; 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; @@ -93,6 +95,18 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + /** + * System property that instructs Spring to ignore class format exceptions during + * classpath scanning, in particular for unsupported class file versions. + * By default, such a class format mismatch leads to a classpath scanning failure. + * @since 6.1.2 + * @see ClassFormatException + */ + public static final String IGNORE_CLASSFORMAT_PROPERTY_NAME = "spring.classformat.ignore"; + + private static final boolean shouldIgnoreClassFormatException = + SpringProperties.getFlag(IGNORE_CLASSFORMAT_PROPERTY_NAME); + protected final Log logger = LogFactory.getLog(getClass()); @@ -102,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; /** @@ -202,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)); @@ -235,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. - } } /** @@ -268,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; } @@ -381,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(); } @@ -480,9 +460,20 @@ private Set scanCandidateComponents(String basePackage) { logger.trace("Ignored non-readable " + resource + ": " + ex.getMessage()); } } + catch (ClassFormatException ex) { + if (shouldIgnoreClassFormatException) { + if (debugEnabled) { + logger.debug("Ignored incompatible class format in " + resource + ": " + ex.getMessage()); + } + } + else { + throw new BeanDefinitionStoreException("Incompatible class format in " + resource + + ": set system property 'spring.classformat.ignore' to 'true' " + + "if you mean to ignore such files during classpath scanning", ex); + } + } catch (Throwable ex) { - throw new BeanDefinitionStoreException( - "Failed to read candidate component class: " + resource, ex); + throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex); } } } @@ -540,9 +531,10 @@ private boolean isConditionMatch(MetadataReader metadataReader) { } /** - * Determine whether the given bean definition qualifies as candidate. - *

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

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

    Can be overridden in subclasses. * @param beanDefinition the bean definition to check * @return whether the bean definition qualifies as a candidate component diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index 32d0cb9451dc..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 @@ -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. @@ -25,6 +25,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -33,8 +34,17 @@ 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; +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.support.ClassHintUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.BeanCreationException; @@ -43,20 +53,27 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor; import org.springframework.beans.factory.annotation.InjectionMetadata; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.EmbeddedValueResolver; import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.AutowireCandidateResolver; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.BridgeMethodResolver; -import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; +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; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -65,7 +82,7 @@ * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation * that supports common Java annotations out of the box, in particular the common * annotations in the {@code jakarta.annotation} package. These common Java - * annotations are supported in many Jakarta EE technologies (e.g. JSF and JAX-RS). + * annotations are supported in many Jakarta EE technologies (for example, JSF and JAX-RS). * *

    This post-processor includes support for the {@link jakarta.annotation.PostConstruct} * and {@link jakarta.annotation.PreDestroy} annotations - as init annotation @@ -80,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. @@ -133,16 +145,11 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean private static final boolean jndiPresent = ClassUtils.isPresent( "javax.naming.InitialContext", CommonAnnotationBeanPostProcessor.class.getClassLoader()); - private static final Set> resourceAnnotationTypes = new LinkedHashSet<>(4); - - @Nullable - private static final Class jakartaResourceType; + private static final Set> resourceAnnotationTypes = CollectionUtils.newLinkedHashSet(3); - @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"); @@ -150,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); @@ -168,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); @@ -196,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(); @@ -298,13 +292,43 @@ public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, C metadata.checkConfigMembers(beanDefinition); } + @Override + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + BeanRegistrationAotContribution parentAotContribution = super.processAheadOfTime(registeredBean); + Class beanClass = registeredBean.getBeanClass(); + String beanName = registeredBean.getBeanName(); + RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); + InjectionMetadata metadata = findResourceMetadata(beanName, beanClass, + beanDefinition.getPropertyValues()); + Collection injectedElements = getInjectedElements(metadata, + beanDefinition.getPropertyValues()); + if (!ObjectUtils.isEmpty(injectedElements)) { + AotContribution aotContribution = new AotContribution(beanClass, injectedElements, + getAutowireCandidateResolver(registeredBean)); + return BeanRegistrationAotContribution.concat(parentAotContribution, aotContribution); + } + return parentAotContribution; + } + + private @Nullable AutowireCandidateResolver getAutowireCandidateResolver(RegisteredBean registeredBean) { + if (registeredBean.getBeanFactory() instanceof DefaultListableBeanFactory lbf) { + return lbf.getAutowireCandidateResolver(); + } + return null; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Collection getInjectedElements(InjectionMetadata metadata, PropertyValues propertyValues) { + return (Collection) metadata.getInjectedElements(propertyValues); + } + @Override public void resetBeanDefinition(String beanName) { this.injectionMetadataCache.remove(beanName); } @Override - public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + public @Nullable Object postProcessBeforeInstantiation(Class beanClass, String beanName) { return null; } @@ -325,6 +349,29 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str return pvs; } + /** + * Native processing method for direct calls with an arbitrary target + * instance, resolving all of its fields and methods which are annotated with + * one of the supported 'resource' annotation types. + * @param bean the target instance to process + * @throws BeanCreationException if resource injection failed + * @since 6.1.3 + */ + public void processInjection(Object bean) throws BeanCreationException { + Class clazz = bean.getClass(); + InjectionMetadata metadata = findResourceMetadata(clazz.getName(), clazz, null); + try { + metadata.inject(bean, null, null); + } + catch (BeanCreationException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanCreationException( + "Injection of resource dependencies failed for class [" + clazz + "]", ex); + } + } + private InjectionMetadata findResourceMetadata(String beanName, Class clazz, @Nullable PropertyValues pvs) { // Fall back to class name as cache key, for backwards compatibility with custom callers. @@ -372,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 -> { @@ -387,8 +426,8 @@ else if (javaxResourceType != null && field.isAnnotationPresent(javaxResourceTyp if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { return; } - if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { - if (ejbAnnotationType != null && bridgedMethod.isAnnotationPresent(ejbAnnotationType)) { + if (ejbAnnotationType != null && bridgedMethod.isAnnotationPresent(ejbAnnotationType)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@EJB annotation is not supported on static methods"); } @@ -398,7 +437,9 @@ else if (javaxResourceType != null && field.isAnnotationPresent(javaxResourceTyp PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); currElements.add(new EjbRefElement(method, bridgedMethod, pd)); } - else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) { + } + else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static methods"); } @@ -411,19 +452,6 @@ else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakart currElements.add(new ResourceElement(method, bridgedMethod, pd)); } } - else if (javaxResourceType != null && bridgedMethod.isAnnotationPresent(javaxResourceType)) { - 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)); - } - } } }); @@ -452,16 +480,9 @@ public Class getTargetClass() { return element.lookupType; } @Override - public boolean isStatic() { - return false; - } - @Override public Object getTarget() { return getResource(element, requestingBeanName); } - @Override - public void releaseTarget(Object target) { - } }; ProxyFactory pf = new ProxyFactory(); @@ -556,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()); @@ -580,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); @@ -606,12 +625,23 @@ public final Class getLookupType() { */ public final DependencyDescriptor getDependencyDescriptor() { if (this.isField) { - return new LookupDependencyDescriptor((Field) this.member, this.lookupType); + return new ResourceElementResolver.LookupDependencyDescriptor( + (Field) this.member, this.lookupType, isLazyLookup()); } else { - return new LookupDependencyDescriptor((Method) this.member, this.lookupType); + return new ResourceElementResolver.LookupDependencyDescriptor( + (Method) this.member, this.lookupType, isLazyLookup()); } } + + /** + * Determine whether this dependency is marked for lazy lookup. + * The default is {@code false}. + * @since 6.1.2 + */ + boolean isLazyLookup() { + return false; + } } @@ -658,51 +688,10 @@ protected Object getResourceToInject(Object target, @Nullable String requestingB return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) : getResource(this, requestingBeanName)); } - } - - - /** - * 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)); + boolean isLazyLookup() { + return this.lazyLookup; } } @@ -764,26 +753,141 @@ else if (this.isDefaultName && !StringUtils.hasLength(this.mappedName)) { /** - * Extension of the DependencyDescriptor class, - * overriding the dependency type with the specified resource type. + * {@link BeanRegistrationAotContribution} to inject resources on fields and methods. */ - private static class LookupDependencyDescriptor extends DependencyDescriptor { + private static class AotContribution implements BeanRegistrationAotContribution { - private final Class lookupType; + private static final String REGISTERED_BEAN_PARAMETER = "registeredBean"; - public LookupDependencyDescriptor(Field field, Class lookupType) { - super(field, true); - this.lookupType = lookupType; - } + private static final String INSTANCE_PARAMETER = "instance"; - public LookupDependencyDescriptor(Method method, Class lookupType) { - super(new MethodParameter(method, 0), true); - this.lookupType = lookupType; + private final Class target; + + private final Collection lookupElements; + + private final @Nullable AutowireCandidateResolver candidateResolver; + + AotContribution(Class target, Collection lookupElements, + @Nullable AutowireCandidateResolver candidateResolver) { + + this.target = target; + this.lookupElements = lookupElements; + this.candidateResolver = candidateResolver; } @Override - public Class getDependencyType() { - return this.lookupType; + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + GeneratedClass generatedClass = generationContext.getGeneratedClasses() + .addForFeatureComponent("ResourceAutowiring", this.target, type -> { + type.addJavadoc("Resource autowiring for {@link $T}.", this.target); + type.addModifiers(javax.lang.model.element.Modifier.PUBLIC); + }); + GeneratedMethod generateMethod = generatedClass.getMethods().add("apply", method -> { + method.addJavadoc("Apply resource autowiring."); + method.addModifiers(javax.lang.model.element.Modifier.PUBLIC, + javax.lang.model.element.Modifier.STATIC); + method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER); + method.addParameter(this.target, INSTANCE_PARAMETER); + method.returns(this.target); + method.addCode(generateMethodCode(generatedClass.getName(), + generationContext.getRuntimeHints())); + }); + beanRegistrationCode.addInstancePostProcessor(generateMethod.toMethodReference()); + + registerHints(generationContext.getRuntimeHints()); + } + + private CodeBlock generateMethodCode(ClassName targetClassName, RuntimeHints hints) { + CodeBlock.Builder code = CodeBlock.builder(); + for (LookupElement lookupElement : this.lookupElements) { + code.addStatement(generateMethodStatementForElement( + targetClassName, lookupElement, hints)); + } + code.addStatement("return $L", INSTANCE_PARAMETER); + return code.build(); + } + + private CodeBlock generateMethodStatementForElement(ClassName targetClassName, + LookupElement lookupElement, RuntimeHints hints) { + + Member member = lookupElement.getMember(); + if (member instanceof Field field) { + return generateMethodStatementForField( + targetClassName, field, lookupElement, hints); + } + if (member instanceof Method method) { + return generateMethodStatementForMethod( + targetClassName, method, lookupElement, hints); + } + throw new IllegalStateException( + "Unsupported member type " + member.getClass().getName()); + } + + private CodeBlock generateMethodStatementForField(ClassName targetClassName, + Field field, LookupElement lookupElement, RuntimeHints hints) { + + hints.reflection().registerField(field); + CodeBlock resolver = generateFieldResolverCode(field, lookupElement); + AccessControl accessControl = AccessControl.forMember(field); + if (!accessControl.isAccessibleFrom(targetClassName)) { + return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, + REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); + } + return CodeBlock.of("$L.$L = $L.resolve($L)", INSTANCE_PARAMETER, + field.getName(), resolver, REGISTERED_BEAN_PARAMETER); + } + + private CodeBlock generateFieldResolverCode(Field field, LookupElement lookupElement) { + if (lookupElement.isDefaultName) { + return CodeBlock.of("$T.$L($S)", ResourceElementResolver.class, + "forField", field.getName()); + } + else { + return CodeBlock.of("$T.$L($S, $S)", ResourceElementResolver.class, + "forField", field.getName(), lookupElement.getName()); + } + } + + private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, + Method method, LookupElement lookupElement, RuntimeHints hints) { + + CodeBlock resolver = generateMethodResolverCode(method, lookupElement); + AccessControl accessControl = AccessControl.forMember(method); + if (!accessControl.isAccessibleFrom(targetClassName)) { + hints.reflection().registerMethod(method, ExecutableMode.INVOKE); + return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, + REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); + } + hints.reflection().registerType(method.getDeclaringClass()); + return CodeBlock.of("$L.$L($L.resolve($L))", INSTANCE_PARAMETER, + method.getName(), resolver, REGISTERED_BEAN_PARAMETER); + + } + + private CodeBlock generateMethodResolverCode(Method method, LookupElement lookupElement) { + if (lookupElement.isDefaultName) { + return CodeBlock.of("$T.$L($S, $T.class)", ResourceElementResolver.class, + "forMethod", method.getName(), lookupElement.getLookupType()); + } + else { + return CodeBlock.of("$T.$L($S, $T.class, $S)", ResourceElementResolver.class, + "forMethod", method.getName(), lookupElement.getLookupType(), lookupElement.getName()); + } + } + + private void registerHints(RuntimeHints runtimeHints) { + this.lookupElements.forEach(lookupElement -> + registerProxyIfNecessary(runtimeHints, lookupElement.getDependencyDescriptor())); + } + + private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescriptor dependencyDescriptor) { + if (this.candidateResolver != null) { + Class proxyClass = + this.candidateResolver.getLazyResolutionProxyClass(dependencyDescriptor, null); + if (proxyClass != null) { + ClassHintUtils.registerProxyIfNecessary(proxyClass, runtimeHints); + } + } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java index 6b9bd448d253..d25e5fb4928e 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java @@ -28,18 +28,21 @@ import org.springframework.core.type.filter.TypeFilter; /** - * Configures component scanning directives for use with @{@link Configuration} classes. - * Provides support parallel with Spring XML's {@code } element. + * Configures component scanning directives for use with {@link Configuration @Configuration} + * classes. + * + *

    Provides support comparable to Spring's {@code } + * XML namespace element. * *

    Either {@link #basePackageClasses} or {@link #basePackages} (or its alias * {@link #value}) may be specified to define specific packages to scan. If specific - * packages are not defined, scanning will occur from the package of the - * class that declares this annotation. + * packages are not defined, scanning will occur recursively beginning with the + * package of the class that declares this annotation. * *

    Note that the {@code } element has an * {@code annotation-config} attribute; however, this annotation does not. This is because * in almost all cases when using {@code @ComponentScan}, default annotation config - * processing (e.g. processing {@code @Autowired} and friends) is assumed. Furthermore, + * processing (for example, processing {@code @Autowired} and friends) is assumed. Furthermore, * when using {@link AnnotationConfigApplicationContext}, annotation config processors are * always registered, meaning that any attempt to disable them at the * {@code @ComponentScan} level would be ignored. @@ -50,6 +53,12 @@ * annotation. {@code @ComponentScan} may also be used as a meta-annotation * to create custom composed annotations with attribute overrides. * + *

    Locally declared {@code @ComponentScan} annotations always take precedence + * over and effectively hide {@code @ComponentScan} meta-annotations, + * which allows explicit local configuration to override configuration that is + * meta-present (including composed annotations meta-annotated with + * {@code @ComponentScan}). + * * @author Chris Beams * @author Juergen Hoeller * @author Sam Brannen @@ -94,7 +103,7 @@ * within the Spring container. *

    The default value of the {@link BeanNameGenerator} interface itself indicates * that the scanner used to process this {@code @ComponentScan} annotation should - * use its inherited bean name generator, e.g. the default + * use its inherited bean name generator, for example, the default * {@link AnnotationBeanNameGenerator} or any custom instance supplied to the * application context at bootstrap time. * @see AnnotationConfigApplicationContext#setBeanNameGenerator(BeanNameGenerator) 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 535717ed400b..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,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,7 @@ 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; import org.springframework.util.StringUtils; @@ -78,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, @@ -112,14 +112,18 @@ protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserCo parseBeanNameGenerator(element, scanner); } catch (Exception ex) { - parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); + String message = ex.getMessage(); + Assert.state(message != null, "Exception message must not be null"); + parserContext.getReaderContext().error(message, parserContext.extractSource(element), ex.getCause()); } try { parseScope(element, scanner); } catch (Exception ex) { - parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); + String message = ex.getMessage(); + Assert.state(message != null, "Exception message must not be null"); + parserContext.getReaderContext().error(message, parserContext.extractSource(element), ex.getCause()); } parseTypeFilters(element, scanner, parserContext); @@ -182,17 +186,11 @@ protected void parseScope(Element element, ClassPathBeanDefinitionScanner scanne if (element.hasAttribute(SCOPED_PROXY_ATTRIBUTE)) { String mode = element.getAttribute(SCOPED_PROXY_ATTRIBUTE); - if ("targetClass".equals(mode)) { - scanner.setScopedProxyMode(ScopedProxyMode.TARGET_CLASS); - } - else if ("interfaces".equals(mode)) { - scanner.setScopedProxyMode(ScopedProxyMode.INTERFACES); - } - else if ("no".equals(mode)) { - scanner.setScopedProxyMode(ScopedProxyMode.NO); - } - else { - throw new IllegalArgumentException("scoped-proxy only supports 'no', 'interfaces' and 'targetClass'"); + switch (mode) { + case "targetClass" -> scanner.setScopedProxyMode(ScopedProxyMode.TARGET_CLASS); + case "interfaces" -> scanner.setScopedProxyMode(ScopedProxyMode.INTERFACES); + case "no" -> scanner.setScopedProxyMode(ScopedProxyMode.NO); + default -> throw new IllegalArgumentException("scoped-proxy only supports 'no', 'interfaces' and 'targetClass'"); } } } @@ -220,8 +218,10 @@ else if (EXCLUDE_FILTER_ELEMENT.equals(localName)) { "Ignoring non-present type filter class: " + ex, parserContext.extractSource(element)); } catch (Exception ex) { + String message = ex.getMessage(); + Assert.state(message != null, "Exception message must not be null"); parserContext.getReaderContext().error( - ex.getMessage(), parserContext.extractSource(element), ex.getCause()); + message, parserContext.extractSource(element), ex.getCause()); } } } @@ -234,28 +234,28 @@ protected TypeFilter createTypeFilter(Element element, @Nullable ClassLoader cla String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE); String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE); expression = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(expression); - if ("annotation".equals(filterType)) { - return new AnnotationTypeFilter((Class) ClassUtils.forName(expression, classLoader)); - } - else if ("assignable".equals(filterType)) { - return new AssignableTypeFilter(ClassUtils.forName(expression, classLoader)); - } - else if ("aspectj".equals(filterType)) { - return new AspectJTypeFilter(expression, classLoader); - } - else if ("regex".equals(filterType)) { - return new RegexPatternTypeFilter(Pattern.compile(expression)); - } - else if ("custom".equals(filterType)) { - Class filterClass = ClassUtils.forName(expression, classLoader); - if (!TypeFilter.class.isAssignableFrom(filterClass)) { - throw new IllegalArgumentException( - "Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression); + switch (filterType) { + case "annotation" -> { + return new AnnotationTypeFilter((Class) ClassUtils.forName(expression, classLoader)); } - return (TypeFilter) BeanUtils.instantiateClass(filterClass); - } - else { - throw new IllegalArgumentException("Unsupported filter type: " + filterType); + case "assignable" -> { + return new AssignableTypeFilter(ClassUtils.forName(expression, classLoader)); + } + case "aspectj" -> { + return new AspectJTypeFilter(expression, classLoader); + } + case "regex" -> { + return new RegexPatternTypeFilter(Pattern.compile(expression)); + } + case "custom" -> { + Class filterClass = ClassUtils.forName(expression, classLoader); + if (!TypeFilter.class.isAssignableFrom(filterClass)) { + throw new IllegalArgumentException( + "Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression); + } + return (TypeFilter) BeanUtils.instantiateClass(filterClass); + } + default -> throw new IllegalArgumentException("Unsupported filter type: " + filterType); } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Condition.java b/spring-context/src/main/java/org/springframework/context/annotation/Condition.java index c38fffc33e25..548018566c82 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Condition.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Condition.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. @@ -20,19 +20,26 @@ import org.springframework.core.type.AnnotatedTypeMetadata; /** - * A single {@code condition} that must be {@linkplain #matches matched} in order - * for a component to be registered. + * A single condition that must be {@linkplain #matches matched} in order for a + * component to be registered. * - *

    Conditions are checked immediately before the bean-definition is due to be + *

    Conditions are checked immediately before the bean definition is due to be * registered and are free to veto registration based on any criteria that can * be determined at that point. * - *

    Conditions must follow the same restrictions as {@link BeanFactoryPostProcessor} + *

    Conditions must follow the same restrictions as a {@link BeanFactoryPostProcessor} * and take care to never interact with bean instances. For more fine-grained control - * of conditions that interact with {@code @Configuration} beans consider implementing + * over conditions that interact with {@code @Configuration} beans, consider implementing * the {@link ConfigurationCondition} interface. * + *

    Multiple conditions on a given class or on a given method will be ordered + * according to the semantics of Spring's {@link org.springframework.core.Ordered} + * interface and {@link org.springframework.core.annotation.Order @Order} annotation. + * See {@link org.springframework.core.annotation.AnnotationAwareOrderComparator} + * for details. + * * @author Phillip Webb + * @author Sam Brannen * @since 4.0 * @see ConfigurationCondition * @see Conditional 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 c97d975e164f..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-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,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; @@ -90,16 +91,7 @@ public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable Co return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } - List conditions = new ArrayList<>(); - for (String[] conditionClasses : getConditionClasses(metadata)) { - for (String conditionClass : conditionClasses) { - Condition condition = getCondition(conditionClass, this.context.getClassLoader()); - conditions.add(condition); - } - } - - AnnotationAwareOrderComparator.sort(conditions); - + List conditions = collectConditions(metadata); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition configurationCondition) { @@ -113,9 +105,31 @@ public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable Co return false; } + /** + * Return the {@linkplain Condition conditions} that should be applied when + * considering the given annotated type. + * @param metadata the metadata of the annotated type + * @return the ordered list of conditions for that type + */ + List collectConditions(@Nullable AnnotatedTypeMetadata metadata) { + if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { + return Collections.emptyList(); + } + + List conditions = new ArrayList<>(); + for (String[] conditionClasses : getConditionClasses(metadata)) { + for (String conditionClass : conditionClasses) { + Condition condition = getCondition(conditionClass, this.context.getClassLoader()); + conditions.add(condition); + } + } + AnnotationAwareOrderComparator.sort(conditions); + return conditions; + } + @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()); } @@ -131,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) { @@ -154,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; } @@ -179,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) { @@ -202,8 +211,7 @@ public BeanDefinitionRegistry getRegistry() { } @Override - @Nullable - public ConfigurableListableBeanFactory getBeanFactory() { + public @Nullable ConfigurableListableBeanFactory getBeanFactory() { return this.beanFactory; } @@ -218,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 b93f1159dd94..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. @@ -443,14 +443,14 @@ /** * Specify whether {@code @Bean} methods should get proxied in order to enforce - * bean lifecycle behavior, e.g. to return shared singleton bean instances even + * bean lifecycle behavior, for example, to return shared singleton bean instances even * in case of direct {@code @Bean} method calls in user code. This feature * requires method interception, implemented through a runtime-generated CGLIB * subclass which comes with limitations such as the configuration class and * its methods not being allowed to declare {@code final}. *

    The default is {@code true}, allowing for 'inter-bean references' via direct * method calls within the configuration class as well as for external calls to - * this configuration's {@code @Bean} methods, e.g. from another configuration class. + * this configuration's {@code @Bean} methods, for example, from another configuration class. * If this is not needed since each of this particular configuration's {@code @Bean} * methods is self-contained and designed as a plain factory method for container use, * switch this flag to {@code false} in order to avoid CGLIB subclass processing. @@ -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 e462008c7c2f..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 @@ -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,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,9 @@ final class ConfigurationClass { private final Resource resource; - @Nullable - private String beanName; + private @Nullable String beanName; + + private boolean scanned = false; private final Set importedBy = new LinkedHashSet<>(1); @@ -63,6 +66,8 @@ final class ConfigurationClass { private final Map> importedResources = new LinkedHashMap<>(); + private final Map beanRegistrars = new LinkedHashMap<>(); + private final Map importBeanDefinitionRegistrars = new LinkedHashMap<>(); @@ -73,7 +78,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param metadataReader reader used to parse the underlying {@link Class} * @param beanName must not be {@code null} - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(MetadataReader metadataReader, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -87,10 +91,10 @@ final class ConfigurationClass { * using the {@link Import} annotation or automatically processed as a nested * configuration class (if importedBy is not {@code null}). * @param metadataReader reader used to parse the underlying {@link Class} - * @param importedBy the configuration class importing this one or {@code null} + * @param importedBy the configuration class importing this one * @since 3.1.1 */ - ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) { + ConfigurationClass(MetadataReader metadataReader, ConfigurationClass importedBy) { this.metadata = metadataReader.getAnnotationMetadata(); this.resource = metadataReader.getResource(); this.importedBy.add(importedBy); @@ -100,7 +104,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param clazz the underlying {@link Class} to represent * @param beanName name of the {@code @Configuration} class bean - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(Class clazz, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -114,10 +117,10 @@ final class ConfigurationClass { * using the {@link Import} annotation or automatically processed as a nested * configuration class (if imported is {@code true}). * @param clazz the underlying {@link Class} to represent - * @param importedBy the configuration class importing this one (or {@code null}) + * @param importedBy the configuration class importing this one * @since 3.1.1 */ - ConfigurationClass(Class clazz, @Nullable ConfigurationClass importedBy) { + ConfigurationClass(Class clazz, ConfigurationClass importedBy) { this.metadata = AnnotationMetadata.introspect(clazz); this.resource = new DescriptiveResource(clazz.getName()); this.importedBy.add(importedBy); @@ -127,13 +130,14 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param metadata the metadata for the underlying class to represent * @param beanName name of the {@code @Configuration} class bean - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) + * @param scanned whether the underlying class has been registered through a scan */ - ConfigurationClass(AnnotationMetadata metadata, String beanName) { + ConfigurationClass(AnnotationMetadata metadata, String beanName, boolean scanned) { Assert.notNull(beanName, "Bean name must not be null"); this.metadata = metadata; this.resource = new DescriptiveResource(metadata.getClassName()); this.beanName = beanName; + this.scanned = scanned; } @@ -149,22 +153,29 @@ String getSimpleName() { return ClassUtils.getShortName(getMetadata().getClassName()); } - void setBeanName(String beanName) { + void setBeanName(@Nullable String beanName) { this.beanName = beanName; } - @Nullable - public String getBeanName() { + @Nullable String getBeanName() { return this.beanName; } + /** + * Return whether this configuration class has been registered through a scan. + * @since 6.2 + */ + boolean isScanned() { + return this.scanned; + } + /** * Return whether this configuration class was registered via @{@link Import} or * automatically registered due to being nested within another configuration class. * @since 3.1.1 * @see #getImportedBy() */ - public boolean isImported() { + boolean isImported() { return !this.importedBy.isEmpty(); } @@ -194,10 +205,31 @@ Set getBeanMethods() { return this.beanMethods; } + boolean hasNonStaticBeanMethods() { + for (BeanMethod beanMethod : this.beanMethods) { + if (!beanMethod.getMetadata().isStatic()) { + return true; + } + } + return false; + } + void addImportedResource(String importedResource, Class readerClass) { this.importedResources.put(importedResource, readerClass); } + 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); } @@ -206,21 +238,18 @@ Map getImportBeanDefinitionRe return this.importBeanDefinitionRegistrars; } - Map> getImportedResources() { - return this.importedResources; - } - + @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 declares proxyBeanMethods=false - if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) { - if (this.metadata.isFinal()) { - problemReporter.error(new FinalConfigurationProblem()); - } - for (BeanMethod beanMethod : this.beanMethods) { - beanMethod.validate(problemReporter); - } + // 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() && + this.metadata.isFinal()) { + problemReporter.error(new FinalConfigurationProblem()); + } + + for (BeanMethod beanMethod : this.beanMethods) { + beanMethod.validate(problemReporter); } // A configuration class may not contain overloaded bean methods unless it declares enforceUniqueMethods=false diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 55d1db9fab90..72776fe44a79 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author 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,11 @@ 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.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; @@ -36,10 +39,11 @@ import org.springframework.beans.factory.parsing.SourceExtractor; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinitionReader; +import org.springframework.beans.factory.support.BeanDefinitionOverrideException; import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.BeanRegistryAdapter; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; @@ -51,8 +55,8 @@ 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; /** @@ -145,7 +149,8 @@ private void loadBeanDefinitionsForConfigurationClass( } loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); - loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); + loadBeanDefinitionsFromImportBeanDefinitionRegistrars(configClass.getImportBeanDefinitionRegistrars()); + loadBeanDefinitionsFromBeanRegistrars(configClass.getBeanRegistrars()); } /** @@ -200,7 +205,7 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { this.registry.registerAlias(beanName, alias); } - // Has this effectively been overridden before (e.g. via XML)? + // Has this effectively been overridden before (for example, via XML)? if (isOverriddenByExistingDefinition(beanMethod, beanName)) { if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) { throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(), @@ -229,8 +234,12 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { beanDef.setUniqueFactoryMethodName(methodName); } - if (metadata instanceof StandardMethodMetadata sam) { - beanDef.setResolvedFactoryMethod(sam.getIntrospectedMethod()); + if (metadata instanceof StandardMethodMetadata smm && + configClass.getMetadata() instanceof StandardAnnotationMetadata sam) { + Method method = ClassUtils.getMostSpecificMethod(smm.getIntrospectedMethod(), sam.getIntrospectedClass()); + if (method == smm.getIntrospectedMethod()) { + beanDef.setResolvedFactoryMethod(method); + } } beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); @@ -241,6 +250,16 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { beanDef.setAutowireCandidate(false); } + boolean defaultCandidate = bean.getBoolean("defaultCandidate"); + if (!defaultCandidate) { + beanDef.setDefaultCandidate(false); + } + + Bean.Bootstrap instantiation = bean.getEnum("bootstrap"); + if (instantiation == Bean.Bootstrap.BACKGROUND) { + beanDef.setBackgroundInit(true); + } + String initMethodName = bean.getString("initMethod"); if (StringUtils.hasText(initMethodName)) { beanDef.setInitMethodName(initMethodName); @@ -277,27 +296,34 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { this.registry.registerBeanDefinition(beanName, beanDefToRegister); } + @SuppressWarnings("NullAway") // Reflection protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String beanName) { if (!this.registry.containsBeanDefinition(beanName)) { return false; } BeanDefinition existingBeanDef = this.registry.getBeanDefinition(beanName); + ConfigurationClass configClass = beanMethod.getConfigurationClass(); - // Is the existing bean definition one that was created from a configuration class? - // -> allow the current bean method to override, since both are at second-pass level. - // However, if the bean method is an overloaded case on the same configuration class, - // preserve the existing bean definition. + // 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( - beanMethod.getConfigurationClass().getMetadata().getClassName())) { - if (ccbd.getFactoryMethodMetadata().getMethodName().equals(ccbd.getFactoryMethodName())) { - ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName()); - } + 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 @@ -318,9 +344,11 @@ protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String // At this point, it's a top-level override (probably XML), just having been parsed // before configuration class processing kicks in... - if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isBeanDefinitionOverridable(beanName)) { - throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(), - beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef); + if (!this.registry.isBeanDefinitionOverridable(beanName)) { + throw new BeanDefinitionOverrideException(beanName, + new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName), + existingBeanDef, + "@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' " + @@ -371,11 +399,19 @@ private void loadBeanDefinitionsFromImportedResources( }); } - 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 @@ -424,7 +460,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 73a922b7cf8f..05b303e455ec 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,10 +24,13 @@ 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; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -37,6 +40,7 @@ import org.springframework.beans.factory.support.SimpleInstantiationStrategy; import org.springframework.cglib.core.ClassGenerator; import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy; +import org.springframework.cglib.core.CodeGenerationException; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.CallbackFilter; @@ -47,7 +51,7 @@ import org.springframework.cglib.proxy.NoOp; import org.springframework.cglib.transform.ClassEmitterTransformer; import org.springframework.cglib.transform.TransformingClassGenerator; -import org.springframework.lang.Nullable; +import org.springframework.core.SmartClassLoader; import org.springframework.objenesis.ObjenesisException; import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.Assert; @@ -99,19 +103,59 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) if (logger.isDebugEnabled()) { logger.debug(String.format("Ignoring request to enhance %s as it has " + "already been enhanced. This usually indicates that more than one " + - "ConfigurationClassPostProcessor has been registered (e.g. via " + + "ConfigurationClassPostProcessor has been registered (for example, via " + "). This is harmless, but you may " + "want check your configuration and remove one CCPP if possible", configClass.getName())); } return configClass; } - Class enhancedClass = createClass(newEnhancer(configClass, classLoader)); - if (logger.isTraceEnabled()) { - logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s", - configClass.getName(), enhancedClass.getName())); + + try { + // Use original ClassLoader if config class not locally loaded in overriding class loader + boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader()); + if (classLoaderMismatch && classLoader instanceof SmartClassLoader smartClassLoader) { + classLoader = smartClassLoader.getOriginalClassLoader(); + classLoaderMismatch = (classLoader != configClass.getClassLoader()); + } + // Use original ClassLoader if config class relies on package visibility + if (classLoaderMismatch && reliesOnPackageVisibility(configClass)) { + classLoader = configClass.getClassLoader(); + classLoaderMismatch = false; + } + Enhancer enhancer = newEnhancer(configClass, classLoader); + Class enhancedClass = createClass(enhancer, classLoaderMismatch); + if (logger.isTraceEnabled()) { + logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s", + configClass.getName(), enhancedClass.getName())); + } + return enhancedClass; + } + catch (CodeGenerationException ex) { + throw new BeanDefinitionStoreException("Could not enhance configuration class [" + configClass.getName() + + "]. Consider declaring @Configuration(proxyBeanMethods=false) without inter-bean references " + + "between @Bean methods on the configuration class, avoiding the need for CGLIB enhancement.", ex); + } + } + + /** + * Checks whether the given config class relies on package visibility, + * either for the class itself or for any of its {@code @Bean} methods. + */ + private boolean reliesOnPackageVisibility(Class configSuperClass) { + int mod = configSuperClass.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; } - return enhancedClass; + for (Method method : ReflectionUtils.getDeclaredMethods(configSuperClass)) { + if (BeanAnnotationHelper.isBeanAnnotated(method)) { + mod = method.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + } + } + return false; } /** @@ -119,11 +163,18 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) */ private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader classLoader) { Enhancer enhancer = new Enhancer(); + if (classLoader != null) { + enhancer.setClassLoader(classLoader); + 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(true); + enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts()); enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader)); enhancer.setCallbackFilter(CALLBACK_FILTER); enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes()); @@ -134,8 +185,21 @@ private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader cl * Uses enhancer to generate a subclass of superclass, * ensuring that callbacks are registered for the new subclass. */ - private Class createClass(Enhancer enhancer) { - Class subclass = enhancer.createClass(); + private Class createClass(Enhancer enhancer, boolean fallback) { + Class subclass; + try { + subclass = enhancer.createClass(); + } + catch (Throwable ex) { + if (!fallback) { + throw (ex instanceof CodeGenerationException cgex ? cgex : new CodeGenerationException(ex)); + } + // Possibly a package-visible @Bean method declaration not accessible + // in the given ClassLoader -> retry with original ClassLoader + enhancer.setClassLoader(null); + subclass = enhancer.createClass(); + } + // Registering callbacks statically (as opposed to thread-local) // is critical for usage in an OSGi environment (SPR-5932)... Enhancer.registerStaticCallbacks(subclass, CALLBACKS); @@ -146,8 +210,7 @@ private Class createClass(Enhancer enhancer) { /** * Marker interface to be implemented by all @Configuration CGLIB subclasses. * Facilitates idempotent behavior for {@link ConfigurationClassEnhancer#enhance} - * through checking to see if candidate classes are already assignable to it, e.g. - * have already been enhanced. + * through checking to see if candidate classes are already assignable to it. *

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

    Note that this interface is intended for framework-internal use only, however @@ -225,7 +288,6 @@ public void end_class() { }; return new TransformingClassGenerator(cg, transformer); } - } @@ -237,8 +299,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]); @@ -280,8 +341,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); @@ -302,9 +362,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 } @@ -334,7 +394,7 @@ public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName); } - 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 @@ -508,7 +568,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 fc7d387e5617..104e2fdd2102 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.util.Comparator; import java.util.Deque; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -36,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; @@ -48,6 +52,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.context.ApplicationContextException; import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; import org.springframework.context.annotation.DeferredImportSelector.Group; import org.springframework.core.OrderComparator; @@ -55,6 +60,7 @@ import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; @@ -66,13 +72,13 @@ 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; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; /** * Parses a {@link Configuration} class definition, populating a collection of @@ -80,10 +86,9 @@ * any number of ConfigurationClass objects because one Configuration class may import * another using the {@link Import} annotation). * - *

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

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

    This ASM-based implementation avoids reflection and eager class loading in order to * interoperate effectively with lazy class loading in a Spring ApplicationContext. @@ -101,6 +106,10 @@ class ConfigurationClassParser { private static final Predicate DEFAULT_EXCLUSION_FILTER = className -> (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())); + private static final Comparator DEFERRED_IMPORT_COMPARATOR = (o1, o2) -> AnnotationAwareOrderComparator.INSTANCE.compare(o1.getImportSelector(), o2.getImportSelector()); @@ -115,8 +124,7 @@ class ConfigurationClassParser { private final ResourceLoader resourceLoader; - @Nullable - private final PropertySourceRegistry propertySourceRegistry; + private final @Nullable PropertySourceRegistry propertySourceRegistry; private final BeanDefinitionRegistry registry; @@ -126,7 +134,7 @@ class ConfigurationClassParser { private final Map configurationClasses = new LinkedHashMap<>(); - private final Map knownSuperclasses = new HashMap<>(); + private final MultiValueMap knownSuperclasses = new LinkedMultiValueMap<>(); private final ImportStack importStack = new ImportStack(); @@ -160,14 +168,23 @@ public void parse(Set configCandidates) { for (BeanDefinitionHolder holder : configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); try { + ConfigurationClass configClass; if (bd instanceof AnnotatedBeanDefinition annotatedBeanDef) { - parse(annotatedBeanDef.getMetadata(), holder.getBeanName()); + configClass = parse(annotatedBeanDef, holder.getBeanName()); } else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef.hasBeanClass()) { - parse(abstractBeanDef.getBeanClass(), holder.getBeanName()); + configClass = parse(abstractBeanDef.getBeanClass(), holder.getBeanName()); } else { - parse(bd.getBeanClassName(), holder.getBeanName()); + configClass = parse(bd.getBeanClassName(), holder.getBeanName()); + } + + // Downgrade to lite (no enhancement) in case of no instance-level @Bean methods. + if (!configClass.getMetadata().isAbstract() && !configClass.hasNonStaticBeanMethods() && + ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( + bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { + bd.setAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE, + ConfigurationClassUtils.CONFIGURATION_CLASS_LITE); } } catch (BeanDefinitionStoreException ex) { @@ -182,31 +199,38 @@ else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef this.deferredImportSelectorHandler.process(); } - protected final void parse(@Nullable String className, String beanName) throws IOException { - Assert.notNull(className, "No bean class name for configuration class bean definition"); - MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); - processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER); + private ConfigurationClass parse(AnnotatedBeanDefinition beanDef, String beanName) { + ConfigurationClass configClass = new ConfigurationClass( + beanDef.getMetadata(), beanName, (beanDef instanceof ScannedGenericBeanDefinition)); + processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER); + return configClass; } - protected final void parse(Class clazz, String beanName) throws IOException { - processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER); + private ConfigurationClass parse(Class clazz, String beanName) { + ConfigurationClass configClass = new ConfigurationClass(clazz, beanName); + processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER); + return configClass; } - protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException { - processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER); + final ConfigurationClass parse(@Nullable String className, String beanName) throws IOException { + Assert.notNull(className, "No bean class name for configuration class bean definition"); + MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); + ConfigurationClass configClass = new ConfigurationClass(reader, beanName); + processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER); + return configClass; } /** * Validate each {@link ConfigurationClass} object. * @see ConfigurationClass#validate */ - public void validate() { + void validate() { for (ConfigurationClass configClass : this.configurationClasses.keySet()) { configClass.validate(this.problemReporter); } } - public Set getConfigurationClasses() { + Set getConfigurationClasses() { return this.configurationClasses.keySet(); } @@ -215,7 +239,12 @@ List getPropertySourceDescriptors() { Collections.emptyList()); } - protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) throws IOException { + ImportRegistry getImportRegistry() { + return this.importStack; + } + + + protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) { if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { return; } @@ -229,11 +258,19 @@ protected void processConfigurationClass(ConfigurationClass configClass, Predica // Otherwise ignore new imported config class; existing non-imported class overrides it. return; } + else if (configClass.isScanned()) { + String beanName = configClass.getBeanName(); + if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { + this.registry.removeBeanDefinition(beanName); + } + // An implicitly scanned bean definition should not override an explicit import. + return; + } else { // Explicit bean definition found, probably replacing an import. // Let's remove the old one and go with the new one. this.configurationClasses.remove(configClass); - this.knownSuperclasses.values().removeIf(configClass::equals); + removeKnownSuperclass(configClass.getMetadata().getClassName(), false); } } @@ -262,8 +299,7 @@ protected void processConfigurationClass(ConfigurationClass configClass, Predica * @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 { @@ -285,11 +321,25 @@ protected final SourceClass doProcessConfigurationClass( } } - // Process any @ComponentScan annotations + // Search for locally declared @ComponentScan annotations first. Set componentScans = AnnotationConfigUtils.attributesForRepeatable( - sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class); - if (!componentScans.isEmpty() && - !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { + sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class, + MergedAnnotation::isDirectlyPresent); + + // Fall back to searching for @ComponentScan meta-annotations (which indirectly + // includes locally declared composed annotations). + if (componentScans.isEmpty()) { + componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), + ComponentScan.class, ComponentScans.class, MergedAnnotation::isMetaPresent); + } + + if (!componentScans.isEmpty()) { + List registerBeanConditions = collectRegisterBeanConditions(configClass); + if (!registerBeanConditions.isEmpty()) { + throw new ApplicationContextException( + "Component scan for configuration class [%s] could not be used with conditions in REGISTER_BEAN phase: %s" + .formatted(configClass.getMetadata().getClassName(), registerBeanConditions)); + } for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set scannedBeanDefinitions = @@ -325,6 +375,9 @@ protected final SourceClass doProcessConfigurationClass( // Process individual @Bean methods Set beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { + if (methodMetadata.isAnnotated("kotlin.jvm.JvmStatic") && !methodMetadata.isStatic()) { + continue; + } configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } @@ -334,11 +387,13 @@ protected final SourceClass doProcessConfigurationClass( // Process superclass, if any if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); - if (superclass != null && !superclass.startsWith("java") && - !this.knownSuperclasses.containsKey(superclass)) { - this.knownSuperclasses.put(superclass, configClass); - // Superclass found, return its annotation metadata and recurse - return sourceClass.getSuperClass(); + if (superclass != null && !superclass.startsWith("java")) { + boolean superclassKnown = this.knownSuperclasses.containsKey(superclass); + this.knownSuperclasses.add(superclass, configClass); + if (!superclassKnown) { + // Superclass found, return its annotation metadata and recurse + return sourceClass.getSuperClass(); + } } } @@ -411,7 +466,7 @@ private Set retrieveBeanMethodMetadata(SourceClass sourceClass) Set asmMethods = asm.getAnnotatedMethods(Bean.class.getName()); if (asmMethods.size() >= beanMethods.size()) { Set candidateMethods = new LinkedHashSet<>(beanMethods); - Set selectedMethods = new LinkedHashSet<>(asmMethods.size()); + Set selectedMethods = CollectionUtils.newLinkedHashSet(asmMethods.size()); for (MethodMetadata asmMethod : asmMethods) { for (Iterator it = candidateMethods.iterator(); it.hasNext();) { MethodMetadata beanMethod = it.next(); @@ -436,14 +491,53 @@ private Set retrieveBeanMethodMetadata(SourceClass sourceClass) return beanMethods; } + /** + * Remove known superclasses for the given removed class, potentially replacing + * the superclass exposure on a different config class with the same superclass. + */ + private void removeKnownSuperclass(String removedClass, boolean replace) { + String replacedSuperclass = null; + ConfigurationClass replacingClass = null; + + Iterator>> it = this.knownSuperclasses.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + if (entry.getValue().removeIf(configClass -> configClass.getMetadata().getClassName().equals(removedClass))) { + if (entry.getValue().isEmpty()) { + it.remove(); + } + else if (replace && replacingClass == null) { + replacedSuperclass = entry.getKey(); + replacingClass = entry.getValue().get(0); + } + } + } + + if (replacingClass != null) { + try { + SourceClass sourceClass = asSourceClass(replacingClass, DEFAULT_EXCLUSION_FILTER).getSuperClass(); + while (!sourceClass.getMetadata().getClassName().equals(replacedSuperclass) && + sourceClass.getMetadata().getSuperClassName() != null) { + sourceClass = sourceClass.getSuperClass(); + } + do { + sourceClass = doProcessConfigurationClass(replacingClass, sourceClass, DEFAULT_EXCLUSION_FILTER); + } + while (sourceClass != null); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "I/O failure while removing configuration class [" + removedClass + "]", ex); + } + } + } /** - * Returns {@code @Import} class, considering all meta-annotations. + * Returns {@code @Import} classes, considering all meta-annotations. */ private Set getImports(SourceClass sourceClass) throws IOException { Set imports = new LinkedHashSet<>(); - Set visited = new LinkedHashSet<>(); - collectImports(sourceClass, imports, visited); + collectImports(sourceClass, imports, new HashSet<>()); return imports; } @@ -475,8 +569,7 @@ private void collectImports(SourceClass sourceClass, Set imports, S } private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, - Collection importCandidates, Predicate exclusionFilter, - boolean checkForCircularImports) { + Collection importCandidates, Predicate filter, boolean checkForCircularImports) { if (importCandidates.isEmpty()) { return; @@ -496,16 +589,25 @@ private void processImports(ConfigurationClass configClass, SourceClass currentS this.environment, this.resourceLoader, this.registry); Predicate selectorFilter = selector.getExclusionFilter(); if (selectorFilter != null) { - exclusionFilter = exclusionFilter.or(selectorFilter); + filter = filter.or(selectorFilter); } if (selector instanceof DeferredImportSelector deferredImportSelector) { this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector); } else { String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); - Collection importSourceClasses = asSourceClasses(importClassNames, exclusionFilter); - processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false); + Collection importSourceClasses = asSourceClasses(importClassNames, filter); + 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 -> @@ -521,7 +623,7 @@ else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // process it as an @Configuration class this.importStack.registerImport( currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); - processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter); + processConfigurationClass(candidate.asConfigClass(configClass), filter); } } } @@ -553,11 +655,6 @@ private boolean isChainedImportOnStack(ConfigurationClass configClass) { return false; } - ImportRegistry getImportRegistry() { - return this.importStack; - } - - /** * Factory method to obtain a {@link SourceClass} from a {@link ConfigurationClass}. */ @@ -596,7 +693,10 @@ SourceClass asSourceClass(@Nullable Class classType, Predicate filter private Collection asSourceClasses(String[] classNames, Predicate filter) throws IOException { List annotatedClasses = new ArrayList<>(classNames.length); for (String className : classNames) { - annotatedClasses.add(asSourceClass(className, filter)); + SourceClass sourceClass = asSourceClass(className, filter); + if (this.objectSourceClass != sourceClass) { + annotatedClasses.add(sourceClass); + } } return annotatedClasses; } @@ -620,19 +720,38 @@ SourceClass asSourceClass(@Nullable String className, Predicate filter) return new SourceClass(this.metadataReaderFactory.getMetadataReader(className)); } + private List collectRegisterBeanConditions(ConfigurationClass configurationClass) { + AnnotationMetadata metadata = configurationClass.getMetadata(); + List allConditions = new ArrayList<>(this.conditionEvaluator.collectConditions(metadata)); + ConfigurationClass enclosingConfigurationClass = getEnclosingConfigurationClass(configurationClass); + if (enclosingConfigurationClass != null) { + allConditions.addAll(this.conditionEvaluator.collectConditions(enclosingConfigurationClass.getMetadata())); + } + return allConditions.stream().filter(REGISTER_BEAN_CONDITION_FILTER).toList(); + } + + private @Nullable ConfigurationClass getEnclosingConfigurationClass(ConfigurationClass configurationClass) { + String enclosingClassName = configurationClass.getMetadata().getEnclosingClassName(); + if (enclosingClassName != null) { + return configurationClass.getImportedBy().stream() + .filter(candidate -> enclosingClassName.equals(candidate.getMetadata().getClassName())) + .findFirst().orElse(null); + } + return null; + } + @SuppressWarnings("serial") - private static class ImportStack extends ArrayDeque implements ImportRegistry { + private class ImportStack extends ArrayDeque implements ImportRegistry { private final MultiValueMap imports = new LinkedMultiValueMap<>(); - public void registerImport(AnnotationMetadata importingClass, String importedClass) { + void registerImport(AnnotationMetadata importingClass, String importedClass) { this.imports.add(importedClass, importingClass); } @Override - @Nullable - public AnnotationMetadata getImportingClassFor(String importedClass) { + public @Nullable AnnotationMetadata getImportingClassFor(String importedClass) { return CollectionUtils.lastElement(this.imports.get(importedClass)); } @@ -646,6 +765,7 @@ public void removeImportingClass(String importingClass) { } } } + removeKnownSuperclass(importingClass, true); } /** @@ -670,8 +790,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 @@ -681,7 +800,7 @@ private class DeferredImportSelectorHandler { * @param configClass the source configuration class * @param importSelector the selector to handle */ - public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) { + void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) { DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector); if (this.deferredImportSelectors == null) { DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler(); @@ -693,7 +812,7 @@ public void handle(ConfigurationClass configClass, DeferredImportSelector import } } - public void process() { + void process() { List deferredImports = this.deferredImportSelectors; this.deferredImportSelectors = null; try { @@ -717,7 +836,7 @@ private class DeferredImportSelectorGroupingHandler { private final Map configurationClasses = new HashMap<>(); - public void register(DeferredImportSelectorHolder deferredImport) { + void register(DeferredImportSelectorHolder deferredImport) { Class group = deferredImport.getImportSelector().getImportGroup(); DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent( (group != null ? group : deferredImport), @@ -727,15 +846,16 @@ public void register(DeferredImportSelectorHolder deferredImport) { deferredImport.getConfigurationClass()); } - public void processGroupImports() { + void processGroupImports() { for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { - Predicate exclusionFilter = grouping.getCandidateFilter(); + 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, exclusionFilter), - Collections.singleton(asSourceClass(entry.getImportClassName(), exclusionFilter)), - exclusionFilter, false); + processImports(configurationClass, asSourceClass(configurationClass, filter), + Collections.singleton(asSourceClass(entry.getImportClassName(), filter)), + filter, false); } catch (BeanDefinitionStoreException ex) { throw ex; @@ -765,16 +885,16 @@ private static class DeferredImportSelectorHolder { private final DeferredImportSelector importSelector; - public DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) { + DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) { this.configurationClass = configClass; this.importSelector = selector; } - public ConfigurationClass getConfigurationClass() { + ConfigurationClass getConfigurationClass() { return this.configurationClass; } - public DeferredImportSelector getImportSelector() { + DeferredImportSelector getImportSelector() { return this.importSelector; } } @@ -790,7 +910,7 @@ private static class DeferredImportSelectorGrouping { this.group = group; } - public void add(DeferredImportSelectorHolder deferredImport) { + void add(DeferredImportSelectorHolder deferredImport) { this.deferredImports.add(deferredImport); } @@ -798,7 +918,7 @@ public void add(DeferredImportSelectorHolder deferredImport) { * Return the imports defined by the group. * @return each import with its associated configuration class */ - public Iterable getImports() { + Iterable getImports() { for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector()); @@ -806,7 +926,7 @@ public Iterable getImports() { return this.group.selectImports(); } - public Predicate getCandidateFilter() { + Predicate getCandidateFilter() { Predicate mergedFilter = DEFAULT_EXCLUSION_FILTER; for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { Predicate selectorFilter = deferredImport.getImportSelector().getExclusionFilter(); @@ -982,12 +1102,12 @@ 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(); } String[] classNames = (String[]) annotationAttributes.get(attribute); - Set result = new LinkedHashSet<>(); + Set result = CollectionUtils.newLinkedHashSet(classNames.length); for (String className : classNames) { result.add(getRelated(className)); } 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 c150656cacc3..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-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.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,8 +83,10 @@ 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; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationStartupAware; import org.springframework.context.EnvironmentAware; @@ -98,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 @@ -152,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(); @@ -168,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; @@ -181,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() { @@ -228,7 +245,7 @@ public void setMetadataReaderFactory(MetadataReaderFactory metadataReaderFactory * class names instead of standard component overriding). *

    Note that this strategy does not apply to {@link Bean} methods. *

    This setter is typically only appropriate when configuring the post-processor as a - * standalone bean definition in XML, e.g. not using the dedicated {@code AnnotationConfig*} + * standalone bean definition in XML, for example, not using the dedicated {@code AnnotationConfig*} * application contexts or the {@code } element. Any bean name * generator specified against the application context will take precedence over any set here. * @since 3.1.1 @@ -311,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)) { @@ -324,11 +340,11 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe } @Override - @Nullable - 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) @@ -337,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); @@ -387,11 +405,11 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. }); // Detect any custom bean name generation strategy supplied through the enclosing application context - SingletonBeanRegistry sbr = null; - if (registry instanceof SingletonBeanRegistry _sbr) { - sbr = _sbr; + SingletonBeanRegistry singletonRegistry = null; + if (registry instanceof SingletonBeanRegistry sbr) { + singletonRegistry = sbr; if (!this.localBeanNameGeneratorSet) { - BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton( + BeanNameGenerator generator = (BeanNameGenerator) singletonRegistry.getSingleton( AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR); if (generator != null) { this.componentScanBeanNameGenerator = generator; @@ -410,7 +428,7 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set candidates = new LinkedHashSet<>(configCandidates); - Set alreadyParsed = new HashSet<>(configCandidates.size()); + Set alreadyParsed = CollectionUtils.newHashSet(configCandidates.size()); do { StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse"); parser.parse(candidates); @@ -426,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(); @@ -433,7 +454,7 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. if (registry.getBeanDefinitionCount() > candidateNames.length) { String[] newCandidateNames = registry.getBeanDefinitionNames(); Set oldCandidateNames = Set.of(candidateNames); - Set alreadyParsedClasses = new HashSet<>(); + Set alreadyParsedClasses = CollectionUtils.newHashSet(alreadyParsed.size()); for (ConfigurationClass configurationClass : alreadyParsed) { alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); } @@ -452,8 +473,8 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. while (!candidates.isEmpty()); // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes - if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { - sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); + if (singletonRegistry != null && !singletonRegistry.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { + singletonRegistry.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); } // Store the PropertySourceDescriptors to contribute them Ahead-of-time if necessary @@ -507,14 +528,18 @@ public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFact throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" + beanName + "' since it is not stored in an AbstractBeanDefinition subclass"); } - else if (logger.isWarnEnabled() && beanFactory.containsSingleton(beanName)) { - logger.warn("Cannot enhance @Configuration bean definition '" + beanName + - "' since its singleton instance has been created too early. The typical cause " + - "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " + - "return type: Consider declaring such methods as 'static' and/or marking the " + - "containing configuration class as 'proxyBeanMethods=false'."); + else if (beanFactory.containsSingleton(beanName)) { + if (logger.isWarnEnabled()) { + logger.warn("Cannot enhance @Configuration bean definition '" + beanName + + "' since its singleton instance has been created too early. The typical cause " + + "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " + + "return type: Consider declaring such methods as 'static' and/or marking the " + + "containing configuration class as 'proxyBeanMethods=false'."); + } + } + else { + configBeanDefs.put(beanName, abd); } - configBeanDefs.put(beanName, abd); } } if (configBeanDefs.isEmpty()) { @@ -552,7 +577,7 @@ public ImportAwareBeanPostProcessor(BeanFactory beanFactory) { } @Override - 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) { @@ -647,9 +672,9 @@ private Map buildImportAwareMappings() { } return mappings; } - } + private static class PropertySourcesAotContribution implements BeanFactoryInitializationAotContribution { private static final String ENVIRONMENT_VARIABLE = "environment"; @@ -660,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; } @@ -754,32 +779,30 @@ private CodeBlock generatePropertySourceDescriptorCode(PropertySourceDescriptor } private CodeBlock handleNull(@Nullable Object value, Supplier nonNull) { - if (value == null) { - return CodeBlock.of("null"); - } - else { - return nonNull.get(); - } + return (value == null ? CodeBlock.of("null") : nonNull.get()); } - } + private static class ConfigurationClassProxyBeanRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator { private final RegisteredBean registeredBean; private final Class proxyClass; - public ConfigurationClassProxyBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments, - RegisteredBean registeredBean) { + public ConfigurationClassProxyBeanRegistrationCodeFragments( + BeanRegistrationCodeFragments codeFragments, RegisteredBean registeredBean) { + super(codeFragments); this.registeredBean = registeredBean; this.proxyClass = registeredBean.getBeanType().toClass(); } @Override - public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, Predicate attributeFilter) { + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { + CodeBlock.Builder code = CodeBlock.builder(); code.add(super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, beanDefinition, attributeFilter)); @@ -789,30 +812,231 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext gener } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { - Executable executableToUse = proxyExecutable(generationContext.getRuntimeHints(), - this.registeredBean.resolveConstructorOrFactoryMethod()); + InstantiationDescriptor instantiationDescriptor = proxyInstantiationDescriptor( + generationContext.getRuntimeHints(), this.registeredBean.resolveInstantiationDescriptor()); + return new InstanceSupplierCodeGenerator(generationContext, beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut) - .generateCode(this.registeredBean, executableToUse); + .generateCode(this.registeredBean, instantiationDescriptor); } - private Executable proxyExecutable(RuntimeHints runtimeHints, Executable userExecutable) { + private InstantiationDescriptor proxyInstantiationDescriptor( + RuntimeHints runtimeHints, InstantiationDescriptor instantiationDescriptor) { + + Executable userExecutable = instantiationDescriptor.executable(); if (userExecutable instanceof Constructor userConstructor) { try { - runtimeHints.reflection().registerConstructor(userConstructor, ExecutableMode.INTROSPECT); - return this.proxyClass.getConstructor(userExecutable.getParameterTypes()); + runtimeHints.reflection().registerType(userConstructor.getDeclaringClass()); + Constructor constructor = this.proxyClass.getConstructor(userExecutable.getParameterTypes()); + return new InstantiationDescriptor(constructor); } catch (NoSuchMethodException ex) { throw new IllegalStateException("No matching constructor found on proxy " + this.proxyClass, ex); } } - return userExecutable; + return instantiationDescriptor; + } + } + + 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 f50fdff6a1e8..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 @@ -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,6 +16,7 @@ package org.springframework.context.annotation; +import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Collection; @@ -25,17 +26,16 @@ 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.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** * Complete implementation of the @@ -49,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); } @@ -85,53 +83,13 @@ protected Object buildLazyResolutionProxy(DependencyDescriptor descriptor, @Null } private Object buildLazyResolutionProxy( - final DependencyDescriptor descriptor, final @Nullable String beanName, boolean classOnly) { + DependencyDescriptor descriptor, @Nullable String beanName, boolean classOnly) { - BeanFactory beanFactory = getBeanFactory(); - Assert.state(beanFactory instanceof DefaultListableBeanFactory, - "BeanFactory needs to be a DefaultListableBeanFactory"); - final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory; + if (!(getBeanFactory() instanceof DefaultListableBeanFactory dlbf)) { + throw new IllegalStateException("Lazy resolution only supported with DefaultListableBeanFactory"); + } - TargetSource ts = new TargetSource() { - @Override - public Class getTargetClass() { - return descriptor.getDependencyType(); - } - @Override - public boolean isStatic() { - return false; - } - @Override - public Object getTarget() { - Set autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null); - Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null); - if (target == null) { - Class type = getTargetClass(); - if (Map.class == type) { - return Collections.emptyMap(); - } - else if (List.class == type) { - return Collections.emptyList(); - } - else if (Set.class == type || Collection.class == type) { - return Collections.emptySet(); - } - throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(), - "Optional dependency not present for lazy injection point"); - } - if (autowiredBeanNames != null) { - for (String autowiredBeanName : autowiredBeanNames) { - if (dlbf.containsBean(autowiredBeanName)) { - dlbf.registerDependentBean(autowiredBeanName, beanName); - } - } - } - return target; - } - @Override - public void releaseTarget(Object target) { - } - }; + TargetSource ts = new LazyDependencyTargetSource(dlbf, descriptor, beanName); ProxyFactory pf = new ProxyFactory(); pf.setTargetSource(ts); @@ -143,4 +101,93 @@ public void releaseTarget(Object target) { return (classOnly ? pf.getProxyClass(classLoader) : pf.getProxy(classLoader)); } + + @SuppressWarnings("serial") + private static class LazyDependencyTargetSource implements TargetSource, Serializable { + + private final DefaultListableBeanFactory beanFactory; + + private final DependencyDescriptor descriptor; + + private final @Nullable String beanName; + + private transient volatile @Nullable Object cachedTarget; + + public LazyDependencyTargetSource(DefaultListableBeanFactory beanFactory, + DependencyDescriptor descriptor, @Nullable String beanName) { + + this.beanFactory = beanFactory; + this.descriptor = descriptor; + this.beanName = beanName; + } + + @Override + public Class getTargetClass() { + return this.descriptor.getDependencyType(); + } + + @Override + public Object getTarget() { + Object cachedTarget = this.cachedTarget; + if (cachedTarget != null) { + return cachedTarget; + } + + Set autowiredBeanNames = new LinkedHashSet<>(2); + Object target = this.beanFactory.doResolveDependency( + this.descriptor, this.beanName, autowiredBeanNames, null); + + if (target == null) { + Class type = getTargetClass(); + if (Map.class == type) { + target = Collections.emptyMap(); + } + else if (List.class == type) { + target = Collections.emptyList(); + } + else if (Set.class == type || Collection.class == type) { + target = Collections.emptySet(); + } + else { + throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType(), + "Optional dependency not present for lazy injection point"); + } + } + else { + if (target instanceof Map map && Map.class == getTargetClass()) { + target = Collections.unmodifiableMap(map); + } + else if (target instanceof List list && List.class == getTargetClass()) { + target = Collections.unmodifiableList(list); + } + else if (target instanceof Set set && Set.class == getTargetClass()) { + target = Collections.unmodifiableSet(set); + } + else if (target instanceof Collection coll && Collection.class == getTargetClass()) { + target = Collections.unmodifiableCollection(coll); + } + } + + boolean cacheable = true; + for (String autowiredBeanName : autowiredBeanNames) { + if (!this.beanFactory.containsBean(autowiredBeanName)) { + cacheable = false; + } + else { + if (this.beanName != null) { + this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName); + } + if (!this.beanFactory.isSingleton(autowiredBeanName)) { + cacheable = false; + } + } + if (cacheable) { + this.cachedTarget = target; + } + } + + return target; + } + } + } 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/EnableAspectJAutoProxy.java b/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java index 62cb6404c493..e14729c06e92 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java @@ -103,7 +103,7 @@ * * Note: {@code @EnableAspectJAutoProxy} applies to its local application context only, * allowing for selective proxying of beans at different levels. Please redeclare - * {@code @EnableAspectJAutoProxy} in each individual context, e.g. the common root web + * {@code @EnableAspectJAutoProxy} 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. * diff --git a/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java b/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java index e31c02d0c670..b0c3aea5fc40 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -92,7 +92,7 @@ * * *

    The code example differs from the XML example in that it actually instantiates the - * {@code MyLoadTimeWeaver} type, meaning that it can also configure the instance, e.g. + * {@code MyLoadTimeWeaver} type, meaning that it can also configure the instance, for example, * calling the {@code #addClassTransformer} method. This demonstrates how the code-based * configuration approach is more flexible through direct programmatic access. * @@ -165,7 +165,7 @@ enum AspectJWeaving { * is present in the classpath. If there is no such resource, then AspectJ * load-time weaving will be switched off. */ - AUTODETECT; + AUTODETECT } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java b/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java new file mode 100644 index 000000000000..32544002ce02 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java @@ -0,0 +1,49 @@ +/* + * 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.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a bean qualifies as a fallback autowire candidate. + * This is a companion and alternative to the {@link Primary} annotation. + * + *

    If all beans but one among multiple matching candidates are marked + * as a fallback, the remaining bean will be selected. + * + *

    Just like primary beans, fallback beans only have an effect when + * finding multiple candidates for single injection points. + * All type-matching beans are included when autowiring arrays, + * collections, maps, or ObjectProvider streams. + * + * @author Juergen Hoeller + * @since 6.2 + * @see Primary + * @see Lazy + * @see Bean + * @see org.springframework.beans.factory.config.BeanDefinition#setFallback + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Fallback { + +} 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..c1275801fc06 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,6 +22,8 @@ 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. @@ -57,7 +59,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/ImportRuntimeHints.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java index bc3d4992c542..002355b8c253 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * 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. @@ -22,6 +22,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; /** @@ -44,7 +45,6 @@ * public MyService myService() { * return new MyService(); * } - * * } * *

    If the configuration class above is processed, {@code MyHints} will be @@ -62,9 +62,8 @@ * @author Brian Clozel * @author Stephane Nicoll * @since 6.0 - * @see org.springframework.aot.hint.RuntimeHints - * @see org.springframework.aot.hint.annotation.Reflective - * @see org.springframework.aot.hint.annotation.RegisterReflectionForBinding + * @see RuntimeHints + * @see ReflectiveScan @ReflectiveScan */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) 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 b3e1c6736ef1..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: @@ -73,12 +74,11 @@ public final void registerScope(String annotationType, String scopeName) { /** * Resolve the given annotation type into a named Spring scope. *

    The default implementation simply checks against registered scopes. - * Can be overridden for custom mapping rules, e.g. naming conventions. + * Can be overridden for custom mapping rules, for example, naming conventions. * @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/Lazy.java b/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java index 19ff1eb1d982..2aad69b606ec 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Lazy.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. @@ -45,7 +45,8 @@ *

    In addition to its role for component initialization, this annotation may also be placed * on injection points marked with {@link org.springframework.beans.factory.annotation.Autowired} * or {@link jakarta.inject.Inject}: In that context, it leads to the creation of a - * lazy-resolution proxy for all affected dependencies, as an alternative to using + * lazy-resolution proxy for the affected dependency, caching it on first access in case of + * a singleton or re-resolving it on every access otherwise. This is an alternative to using * {@link org.springframework.beans.factory.ObjectFactory} or {@link jakarta.inject.Provider}. * Please note that such a lazy-resolution proxy will always be injected; if the target * dependency does not exist, you will only be able to find out through an exception on 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 477de756a8c7..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -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 @@ -94,20 +92,20 @@ public LoadTimeWeaver loadTimeWeaver() { if (this.enableLTW != null) { AspectJWeaving aspectJWeaving = this.enableLTW.getEnum("aspectjWeaving"); switch (aspectJWeaving) { - case DISABLED: + case DISABLED -> { // AJ weaving is disabled -> do nothing - break; - case AUTODETECT: + } + case AUTODETECT -> { if (this.beanClassLoader.getResource(AspectJWeavingEnabler.ASPECTJ_AOP_XML_RESOURCE) == null) { // No aop.xml present on the classpath -> treat as 'disabled' break; } // aop.xml is present on the classpath -> enable AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader); - break; - case ENABLED: + } + case ENABLED -> { AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader); - break; + } } } 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 8decf18cfad3..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,12 +32,11 @@ 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; /** - * Common delegate code for the handling of parser strategies, e.g. - * {@code TypeFilter}, {@code ImportSelector}, {@code ImportBeanDefinitionRegistrar} + * Common delegate code for the handling of parser strategies, for example, + * {@code TypeFilter}, {@code ImportSelector}, {@code ImportBeanDefinitionRegistrar}. * * @author Juergen Hoeller * @author Phillip Webb @@ -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/Primary.java b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java index 3832996e448d..92c26789f4ae 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,10 @@ * are qualified to autowire a single-valued dependency. If exactly one * 'primary' bean exists among the candidates, it will be the autowired value. * + *

    Primary beans only have an effect when finding multiple candidates + * for single injection points. All type-matching beans are included when + * autowiring arrays, collections, maps, or ObjectProvider streams. + * *

    This annotation is semantically equivalent to the {@code } element's * {@code primary} attribute in Spring XML. * @@ -77,10 +81,12 @@ * @author Chris Beams * @author Juergen Hoeller * @since 3.0 + * @see Fallback * @see Lazy * @see Bean * @see ComponentScan * @see org.springframework.stereotype.Component + * @see org.springframework.beans.factory.config.BeanDefinition#setPrimary */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Profile.java b/spring-context/src/main/java/org/springframework/context/annotation/Profile.java index b128bf9208c0..9575ef1aa8fc 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Profile.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Profile.java @@ -63,7 +63,7 @@ * details about supported formats. * *

    This is analogous to the behavior in Spring XML: if the {@code profile} attribute of - * the {@code beans} element is supplied e.g., {@code }, the + * the {@code beans} element is supplied, for example, {@code }, the * {@code beans} element will not be parsed unless at least profile 'p1' or 'p2' has been * activated. Likewise, if a {@code @Component} or {@code @Configuration} class is marked * with {@code @Profile({"p1", "p2"})}, that class will not be registered or processed unless 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 cc9b664921de..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,8 +33,9 @@ class ProfileCondition implements Condition { @Override + @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/PropertySource.java b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java index d6e5dc4339ab..c45ab857dbb2 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java @@ -222,7 +222,7 @@ boolean ignoreResourceNotFound() default false; /** - * A specific character encoding for the given resources, e.g. "UTF-8". + * A specific character encoding for the given resources, for example, "UTF-8". * @since 4.3 */ String encoding() default ""; diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java new file mode 100644 index 000000000000..efff134d6dd5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java @@ -0,0 +1,90 @@ +/* + * 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.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.aot.hint.annotation.Reflective; +import org.springframework.aot.hint.annotation.RegisterReflection; +import org.springframework.core.annotation.AliasFor; + +/** + * Scan arbitrary types for use of {@link Reflective}. Typically used on + * {@link Configuration @Configuration} classes but can be added to any bean. + * Scanning happens during AOT processing, typically at build-time. + * + *

    In the example below, {@code com.example.app} and its subpackages are + * scanned:

    
    + * @Configuration
    + * @ReflectiveScan("com.example.app")
    + * class MyConfiguration {
    + *     // ...
    + * }
    + * + *

    Either {@link #basePackageClasses} or {@link #basePackages} (or its alias + * {@link #value}) may be specified to define specific packages to scan. If specific + * packages are not defined, scanning will occur recursively beginning with the + * package of the class that declares this annotation. + * + *

    A type does not need to be annotated at class level to be candidate, and + * this performs a "deep scan" by loading every class in the target packages and + * search for {@link Reflective} on types, constructors, methods, and fields. + * Enclosed classes are candidates as well. Classes that fail to load are + * ignored. + * + * @author Stephane Nicoll + * @see Reflective @Reflective + * @see RegisterReflection @RegisterReflection + * @since 6.2 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +public @interface ReflectiveScan { + + /** + * Alias for {@link #basePackages}. + *

    Allows for more concise annotation declarations if no other attributes + * are needed — for example, {@code @ReflectiveScan("org.my.pkg")} + * instead of {@code @ReflectiveScan(basePackages = "org.my.pkg")}. + */ + @AliasFor("basePackages") + String[] value() default {}; + + /** + * Base packages to scan for reflective usage. + *

    {@link #value} is an alias for (and mutually exclusive with) this + * attribute. + *

    Use {@link #basePackageClasses} for a type-safe alternative to + * String-based package names. + */ + @AliasFor("value") + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages} for specifying the packages + * to scan for reflection usage. The package of each class specified will be scanned. + *

    Consider creating a special no-op marker class or interface in each package + * that serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; + +} 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 new file mode 100644 index 000000000000..eaf2a86d7508 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ResourceElementResolver.java @@ -0,0 +1,330 @@ +/* + * 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.context.annotation; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +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; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Resolver for the injection of named beans on a field or method element, + * following the rules of the {@link jakarta.annotation.Resource} annotation + * but without any JNDI support. This is primarily intended for AOT processing. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 6.1.2 + * @see CommonAnnotationBeanPostProcessor + * @see jakarta.annotation.Resource + */ +public abstract class ResourceElementResolver { + + private final String name; + + private final boolean defaultName; + + + ResourceElementResolver(String name, boolean defaultName) { + this.name = name; + this.defaultName = defaultName; + } + + + /** + * Create a new {@link ResourceFieldResolver} for the specified field. + * @param fieldName the field name + * @return a new {@link ResourceFieldResolver} instance + */ + public static ResourceElementResolver forField(String fieldName) { + return new ResourceFieldResolver(fieldName, true, fieldName); + } + + /** + * Create a new {@link ResourceFieldResolver} for the specified field and resource name. + * @param fieldName the field name + * @param resourceName the resource name + * @return a new {@link ResourceFieldResolver} instance + */ + public static ResourceElementResolver forField(String fieldName, String resourceName) { + return new ResourceFieldResolver(resourceName, false, fieldName); + } + + /** + * Create a new {@link ResourceMethodResolver} for the specified method + * using a resource name that infers from the method name. + * @param methodName the method name + * @param parameterType the parameter type. + * @return a new {@link ResourceMethodResolver} instance + */ + public static ResourceElementResolver forMethod(String methodName, Class parameterType) { + return new ResourceMethodResolver(defaultResourceNameForMethod(methodName), true, + methodName, parameterType); + } + + /** + * Create a new {@link ResourceMethodResolver} for the specified method + * and resource name. + * @param methodName the method name + * @param parameterType the parameter type + * @param resourceName the resource name + * @return a new {@link ResourceMethodResolver} instance + */ + public static ResourceElementResolver forMethod(String methodName, Class parameterType, String resourceName) { + return new ResourceMethodResolver(resourceName, false, methodName, parameterType); + } + + private static String defaultResourceNameForMethod(String methodName) { + if (methodName.startsWith("set") && methodName.length() > 3) { + return StringUtils.uncapitalizeAsProperty(methodName.substring(3)); + } + return methodName; + } + + + /** + * Resolve the value for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved field or method parameter value + */ + @SuppressWarnings("unchecked") + public @Nullable T resolve(RegisteredBean registeredBean) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + return (T) (isLazyLookup(registeredBean) ? buildLazyResourceProxy(registeredBean) : + resolveValue(registeredBean)); + } + + /** + * Resolve the value for the specified registered bean and set it using reflection. + * @param registeredBean the registered bean + * @param instance the bean instance + */ + public abstract void resolveAndSet(RegisteredBean registeredBean, Object instance); + + /** + * Create a suitable {@link DependencyDescriptor} for the specified bean. + * @param registeredBean the registered bean + * @return a descriptor for that bean + */ + abstract DependencyDescriptor createDependencyDescriptor(RegisteredBean registeredBean); + + abstract Class getLookupType(RegisteredBean registeredBean); + + abstract AnnotatedElement getAnnotatedElement(RegisteredBean registeredBean); + + boolean isLazyLookup(RegisteredBean registeredBean) { + AnnotatedElement ae = getAnnotatedElement(registeredBean); + Lazy lazy = ae.getAnnotation(Lazy.class); + return (lazy != null && lazy.value()); + } + + private Object buildLazyResourceProxy(RegisteredBean registeredBean) { + Class lookupType = getLookupType(registeredBean); + + TargetSource ts = new TargetSource() { + @Override + public Class getTargetClass() { + return lookupType; + } + @Override + public Object getTarget() { + return resolveValue(registeredBean); + } + }; + + ProxyFactory pf = new ProxyFactory(); + pf.setTargetSource(ts); + if (lookupType.isInterface()) { + pf.addInterface(lookupType); + } + return pf.getProxy(registeredBean.getBeanFactory().getBeanClassLoader()); + } + + /** + * Resolve the value to inject for this instance. + * @param registeredBean the bean registration + * @return the value to inject + */ + private Object resolveValue(RegisteredBean registeredBean) { + ConfigurableListableBeanFactory factory = registeredBean.getBeanFactory(); + + Object resource; + Set autowiredBeanNames; + DependencyDescriptor descriptor = createDependencyDescriptor(registeredBean); + if (this.defaultName && !factory.containsBean(this.name)) { + autowiredBeanNames = new LinkedHashSet<>(); + resource = factory.resolveDependency(descriptor, registeredBean.getBeanName(), autowiredBeanNames, null); + if (resource == null) { + throw new NoSuchBeanDefinitionException(descriptor.getDependencyType(), "No resolvable resource object"); + } + } + else { + resource = factory.resolveBeanByName(this.name, descriptor); + autowiredBeanNames = Collections.singleton(this.name); + } + + for (String autowiredBeanName : autowiredBeanNames) { + if (factory.containsBean(autowiredBeanName)) { + factory.registerDependentBean(autowiredBeanName, registeredBean.getBeanName()); + } + } + return resource; + } + + + private static final class ResourceFieldResolver extends ResourceElementResolver { + + private final String fieldName; + + public ResourceFieldResolver(String name, boolean defaultName, String fieldName) { + super(name, defaultName); + this.fieldName = fieldName; + } + + @Override + public void resolveAndSet(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(instance, "'instance' must not be null"); + Field field = getField(registeredBean); + Object resolved = resolve(registeredBean); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, instance, resolved); + } + + @Override + protected DependencyDescriptor createDependencyDescriptor(RegisteredBean registeredBean) { + Field field = getField(registeredBean); + return new LookupDependencyDescriptor(field, field.getType(), isLazyLookup(registeredBean)); + } + + @Override + protected Class getLookupType(RegisteredBean registeredBean) { + return getField(registeredBean).getType(); + } + + @Override + protected AnnotatedElement getAnnotatedElement(RegisteredBean registeredBean) { + return getField(registeredBean); + } + + private Field getField(RegisteredBean registeredBean) { + Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), this.fieldName); + Assert.notNull(field, + () -> "No field '" + this.fieldName + "' found on " + registeredBean.getBeanClass().getName()); + return field; + } + } + + + private static final class ResourceMethodResolver extends ResourceElementResolver { + + private final String methodName; + + private final Class lookupType; + + private ResourceMethodResolver(String name, boolean defaultName, String methodName, Class lookupType) { + super(name, defaultName); + this.methodName = methodName; + this.lookupType = lookupType; + } + + @Override + public void resolveAndSet(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(instance, "'instance' must not be null"); + Method method = getMethod(registeredBean); + Object resolved = resolve(registeredBean); + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, instance, resolved); + } + + @Override + protected DependencyDescriptor createDependencyDescriptor(RegisteredBean registeredBean) { + return new LookupDependencyDescriptor( + getMethod(registeredBean), this.lookupType, isLazyLookup(registeredBean)); + } + + @Override + protected Class getLookupType(RegisteredBean bean) { + return this.lookupType; + } + + @Override + protected AnnotatedElement getAnnotatedElement(RegisteredBean registeredBean) { + return getMethod(registeredBean); + } + + private Method getMethod(RegisteredBean registeredBean) { + Method method = ReflectionUtils.findMethod(registeredBean.getBeanClass(), this.methodName, this.lookupType); + Assert.notNull(method, + () -> "Method '%s' with parameter type '%s' declared on %s could not be found.".formatted( + this.methodName, this.lookupType.getName(), registeredBean.getBeanClass().getName())); + return method; + } + } + + + /** + * Extension of the DependencyDescriptor class, + * overriding the dependency type with the specified resource type. + */ + @SuppressWarnings("serial") + static class LookupDependencyDescriptor extends DependencyDescriptor { + + private final Class lookupType; + + private final boolean lazyLookup; + + public LookupDependencyDescriptor(Field field, Class lookupType, boolean lazyLookup) { + super(field, true); + this.lookupType = lookupType; + this.lazyLookup = lazyLookup; + } + + public LookupDependencyDescriptor(Method method, Class lookupType, boolean lazyLookup) { + super(new MethodParameter(method, 0), true); + this.lookupType = lookupType; + this.lazyLookup = lazyLookup; + } + + @Override + public Class getDependencyType() { + return this.lookupType; + } + + @Override + public boolean supportsLazyResolution() { + return !this.lazyLookup; + } + } + +} 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/ScopedProxyMode.java b/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java index 08e7fd8e32a1..d1f6df1d1b20 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.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. @@ -20,8 +20,8 @@ * Enumerates the various scoped-proxy options. * *

    For a more complete discussion of exactly what a scoped proxy is, see the - * section of the Spring reference documentation entitled 'Scoped beans as - * dependencies'. + * Scoped Beans as Dependencies section of the Spring reference documentation. * * @author Mark Fisher * @since 2.5 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 9acfa463e246..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-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. @@ -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; @@ -47,11 +49,11 @@ public abstract class AbstractAotProcessor { /** - * The name of a system property that is made available when the processor - * runs. + * The name of a system property that is made available when the processor runs. + * @since 6.2 * @see #doProcess() */ - private static final String AOT_PROCESSING = "spring.aot.processing"; + public static final String AOT_PROCESSING = "spring.aot.processing"; private final Settings settings; @@ -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); } } } @@ -125,6 +127,7 @@ protected void writeHints(RuntimeHints hints) { writer.write(hints); } + /** * Common settings for AOT processors. */ @@ -140,7 +143,6 @@ public static final class Settings { private final String artifactId; - private Settings(Path sourceOutput, Path resourceOutput, Path classOutput, String groupId, String artifactId) { this.sourceOutput = sourceOutput; this.resourceOutput = resourceOutput; @@ -149,7 +151,6 @@ private Settings(Path sourceOutput, Path resourceOutput, Path classOutput, Strin this.artifactId = artifactId; } - /** * Create a new {@link Builder} for {@link Settings}. */ @@ -157,7 +158,6 @@ public static Builder builder() { return new Builder(); } - /** * Get the output directory for generated sources. */ @@ -199,27 +199,20 @@ 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; - - @Nullable - private String artifactId; + private @Nullable String groupId; + private @Nullable String artifactId; private Builder() { // internal constructor } - /** * Set the output directory for generated sources. * @param sourceOutput the location of generated sources @@ -257,6 +250,7 @@ public Builder classOutput(Path classOutput) { * @return this builder for method chaining */ public Builder groupId(String groupId) { + Assert.hasText(groupId, "'groupId' must not be empty"); this.groupId = groupId; return this; } @@ -268,6 +262,7 @@ public Builder groupId(String groupId) { * @return this builder for method chaining */ public Builder artifactId(String artifactId) { + Assert.hasText(artifactId, "'artifactId' must not be empty"); this.artifactId = artifactId; return this; } @@ -279,14 +274,12 @@ public Settings build() { Assert.notNull(this.sourceOutput, "'sourceOutput' must not be null"); Assert.notNull(this.resourceOutput, "'resourceOutput' must not be null"); Assert.notNull(this.classOutput, "'classOutput' must not be null"); - Assert.hasText(this.groupId, "'groupId' must not be null or empty"); - Assert.hasText(this.artifactId, "'artifactId' must not be null or empty"); + Assert.notNull(this.groupId, "'groupId' must not be null"); + Assert.notNull(this.artifactId, "'artifactId' must not be null"); return new Settings(this.sourceOutput, this.resourceOutput, this.classOutput, this.groupId, this.artifactId); } - } - } } 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/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 878ed204c0b4..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,7 +20,11 @@ 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; import org.springframework.beans.factory.aot.AotServices; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; @@ -63,8 +67,7 @@ private List getContributions( List processors) { List contributions = new ArrayList<>(); for (BeanFactoryInitializationAotProcessor processor : processors) { - BeanFactoryInitializationAotContribution contribution = processor - .processAheadOfTime(beanFactory); + BeanFactoryInitializationAotContribution contribution = processAheadOfTime(processor, beanFactory); if (contribution != null) { contributions.add(contribution); } @@ -72,6 +75,21 @@ private List getContributions( return Collections.unmodifiableList(contributions); } + private @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(BeanFactoryInitializationAotProcessor processor, + DefaultListableBeanFactory beanFactory) { + + try { + return processor.processAheadOfTime(beanFactory); + } + catch (AotException ex) { + throw ex; + } + catch (Exception ex) { + throw new AotProcessingException("Error executing '" + + processor.getClass().getName() + "': " + ex.getMessage(), ex); + } + } + void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { for (BeanFactoryInitializationAotContribution contribution : this.contributions) { 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 320bde37ff84..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 @@ -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,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 @@ -35,9 +35,8 @@ */ class KotlinReflectionBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { - @Nullable @Override - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); if (KotlinDetector.isKotlinType(beanClass)) { return new AotContribution(beanClass); @@ -45,6 +44,7 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe return null; } + private static class AotContribution implements BeanRegistrationAotContribution { private final Class beanClass; @@ -60,12 +60,16 @@ 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) { registerHints(superClass, runtimeHints); } + Class enclosingClass = type.getEnclosingClass(); + if (enclosingClass != null) { + runtimeHints.reflection().registerType(enclosingClass); + } } } 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 new file mode 100644 index 000000000000..579e73e93fd9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java @@ -0,0 +1,159 @@ +/* + * 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.context.aot; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +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; +import org.springframework.aot.hint.annotation.ReflectiveProcessor; +import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; +import org.springframework.aot.hint.annotation.RegisterReflection; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.util.ClassUtils; + +/** + * Builder for an {@linkplain BeanFactoryInitializationAotContribution AOT + * contribution} that detects the presence of {@link Reflective @Reflective} on + * annotated elements and invoke the underlying {@link ReflectiveProcessor} + * implementations. + * + *

    Candidates can be provided explicitly or by scanning the classpath. + * + * @author Stephane Nicoll + * @since 6.2 + * @see Reflective + * @see RegisterReflection + */ +public class ReflectiveProcessorAotContributionBuilder { + + private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); + + private final Set> classes = new LinkedHashSet<>(); + + + /** + * Process the given classes by checking the ones that use {@link Reflective}. + *

    A class is candidate if it uses {@link Reflective} directly or via a + * meta-annotation. Type, fields, constructors, methods and enclosed types + * are inspected. + * @param classes the classes to inspect + */ + public ReflectiveProcessorAotContributionBuilder withClasses(Iterable> classes) { + this.classes.addAll(StreamSupport.stream(classes.spliterator(), false) + .filter(registrar::isCandidate).toList()); + return this; + } + + /** + * Process the given classes by checking the ones that use {@link Reflective}. + *

    A class is candidate if it uses {@link Reflective} directly or via a + * meta-annotation. Type, fields, constructors, methods and enclosed types + * are inspected. + * @param classes the classes to inspect + */ + public ReflectiveProcessorAotContributionBuilder withClasses(Class[] classes) { + return withClasses(Arrays.asList(classes)); + } + + /** + * Scan the given {@code packageNames} and their sub-packages for classes + * that uses {@link Reflective}. + *

    This performs a "deep scan" by loading every class in the specified + * packages and search for {@link Reflective} on types, constructors, methods, + * and fields. Enclosed classes are candidates as well. Classes that fail to + * load are ignored. + * @param classLoader the classloader to use + * @param packageNames the package names to scan + */ + public ReflectiveProcessorAotContributionBuilder scan(@Nullable ClassLoader classLoader, String... packageNames) { + ReflectiveClassPathScanner scanner = new ReflectiveClassPathScanner(classLoader); + return withClasses(scanner.scan(packageNames)); + } + + public @Nullable BeanFactoryInitializationAotContribution build() { + return (!this.classes.isEmpty() ? new AotContribution(this.classes) : null); + } + + private static class AotContribution implements BeanFactoryInitializationAotContribution { + + private final Class[] classes; + + public AotContribution(Set> classes) { + this.classes = classes.toArray(Class[]::new); + } + + @Override + public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + registrar.registerRuntimeHints(runtimeHints, this.classes); + } + + } + + private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider { + + private final @Nullable ClassLoader classLoader; + + ReflectiveClassPathScanner(@Nullable ClassLoader classLoader) { + super(false); + this.classLoader = classLoader; + addIncludeFilter((metadataReader, metadataReaderFactory) -> true); + } + + Class[] scan(String... packageNames) { + if (logger.isDebugEnabled()) { + logger.debug("Scanning all types for reflective usage from " + Arrays.toString(packageNames)); + } + Set candidates = new HashSet<>(); + for (String packageName : packageNames) { + candidates.addAll(findCandidateComponents(packageName)); + } + return candidates.stream().map(c -> (Class) c.getAttribute("type")).toArray(Class[]::new); + } + + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + String className = beanDefinition.getBeanClassName(); + if (className != null) { + try { + Class type = ClassUtils.forName(className, this.classLoader); + beanDefinition.setAttribute("type", type); + return registrar.isCandidate(type); + } + catch (Exception ex) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring '%s' for reflective usage: %s".formatted(className, ex.getMessage())); + } + } + } + return 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 238350ffc226..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 @@ -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. @@ -17,17 +17,21 @@ package org.springframework.context.aot; import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.jspecify.annotations.Nullable; -import org.springframework.aot.generate.GenerationContext; -import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.annotation.Reflective; import org.springframework.aot.hint.annotation.ReflectiveProcessor; -import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; -import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.context.annotation.ReflectiveScan; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ClassUtils; /** * AOT {@code BeanFactoryInitializationAotProcessor} that detects the presence @@ -39,30 +43,37 @@ */ class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { - private static final ReflectiveRuntimeHintsRegistrar REGISTRAR = new ReflectiveRuntimeHintsRegistrar(); - @Override - public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { - Class[] beanTypes = Arrays.stream(beanFactory.getBeanDefinitionNames()) + public @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + Class[] beanClasses = Arrays.stream(beanFactory.getBeanDefinitionNames()) .map(beanName -> RegisteredBean.of(beanFactory, beanName).getBeanClass()) .toArray(Class[]::new); - return new ReflectiveProcessorBeanFactoryInitializationAotContribution(beanTypes); + String[] packagesToScan = findBasePackagesToScan(beanClasses); + return new ReflectiveProcessorAotContributionBuilder().withClasses(beanClasses) + .scan(beanFactory.getBeanClassLoader(), packagesToScan).build(); } - private static class ReflectiveProcessorBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution { - - private final Class[] types; - - public ReflectiveProcessorBeanFactoryInitializationAotContribution(Class[] types) { - this.types = types; + protected String[] findBasePackagesToScan(Class[] beanClasses) { + Set basePackages = new LinkedHashSet<>(); + for (Class beanClass : beanClasses) { + ReflectiveScan reflectiveScan = AnnotatedElementUtils.getMergedAnnotation(beanClass, ReflectiveScan.class); + if (reflectiveScan != null) { + basePackages.addAll(extractBasePackages(reflectiveScan, beanClass)); + } } + return basePackages.toArray(new String[0]); + } - @Override - public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { - RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - REGISTRAR.registerRuntimeHints(runtimeHints, this.types); + private Set extractBasePackages(ReflectiveScan annotation, Class declaringClass) { + Set basePackages = new LinkedHashSet<>(); + Collections.addAll(basePackages, annotation.basePackages()); + for (Class clazz : annotation.basePackageClasses()) { + basePackages.add(ClassUtils.getPackageName(clazz)); } - + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(declaringClass)); + } + return basePackages; } } 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 ffb4c40d6fc0..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 @@ -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. @@ -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,7 @@ 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; /** * {@link BeanFactoryInitializationAotProcessor} implementation that processes @@ -76,8 +77,9 @@ private Set> extractFromBeanFactory(Confi private Set> extractFromBeanDefinition(String beanName, ImportRuntimeHints annotation) { - Set> registrars = new LinkedHashSet<>(); - for (Class registrarClass : annotation.value()) { + Class[] registrarClasses = annotation.value(); + Set> registrars = CollectionUtils.newLinkedHashSet(registrarClasses.length); + for (Class registrarClass : registrarClasses) { if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Loaded [%s] registrar from annotated bean [%s]", registrarClass.getCanonicalName(), beanName)); @@ -92,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/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 76be0c052529..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,9 +39,9 @@ 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; import org.springframework.util.ObjectUtils; /** @@ -69,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 @@ -229,6 +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") // Dataflow analysis limitation private Collection> retrieveApplicationListeners( ResolvableType eventType, @Nullable Class sourceType, @Nullable CachedListenerRetriever retriever) { @@ -296,7 +297,7 @@ private Collection> retrieveApplicationListeners( else { // Remove non-matching listeners that originally came from // ApplicationListenerDetector, possibly ruled out by additional - // BeanDefinition metadata (e.g. factory method generics) above. + // BeanDefinition metadata (for example, factory method generics) above. Object listener = beanFactory.getSingleton(listenerBeanName); if (retriever != null) { filteredListeners.remove(listener); @@ -313,7 +314,7 @@ private Collection> retrieveApplicationListeners( AnnotationAwareOrderComparator.sort(allListeners); if (retriever != null) { - if (filteredListenerBeans.isEmpty()) { + if (CollectionUtils.isEmpty(filteredListenerBeans)) { retriever.applicationListeners = new LinkedHashSet<>(allListeners); retriever.applicationListenerBeans = filteredListenerBeans; } @@ -405,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"); @@ -455,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/ApplicationContextEvent.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java index fab9067b20d6..0823d051c340 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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,7 +29,7 @@ public abstract class ApplicationContextEvent extends ApplicationEvent { /** - * Create a new ContextStartedEvent. + * Create a new {@code ApplicationContextEvent}. * @param source the {@code ApplicationContext} that the event is raised for * (must not be {@code 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 a57899969d7f..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 @@ -73,12 +74,12 @@ public interface ApplicationEventMulticaster { /** * Remove all matching listeners from the set of registered * {@code ApplicationListener} instances (which includes adapter classes - * such as {@link ApplicationListenerMethodAdapter}, e.g. for annotated + * such as {@link ApplicationListenerMethodAdapter}, for example, for annotated * {@link EventListener} methods). *

    Note: This just applies to instance registrations, not to listeners * registered by bean name. * @param predicate the predicate to identify listener instances to remove, - * e.g. checking {@link SmartApplicationListener#getListenerId()} + * for example, checking {@link SmartApplicationListener#getListenerId()} * @since 5.3.5 * @see #addApplicationListener(ApplicationListener) * @see #removeApplicationListener(ApplicationListener) 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 b80bd9a19db4..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 @@ -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. @@ -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; @@ -67,6 +68,7 @@ * @author Juergen Hoeller * @author Sam Brannen * @author Sebastien Deleuze + * @author Yanming Zhou * @since 4.2 */ public class ApplicationListenerMethodAdapter implements GenericApplicationListener { @@ -87,19 +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; /** @@ -118,6 +118,7 @@ public ApplicationListenerMethodAdapter(String beanName, Class targetClass, M EventListener ann = AnnotatedElementUtils.findMergedAnnotation(this.targetMethod, EventListener.class); this.declaredEventTypes = resolveDeclaredEventTypes(method, ann); this.condition = (ann != null ? ann.condition() : null); + this.defaultExecution = (ann == null || ann.defaultExecution()); this.order = resolveOrder(this.targetMethod); String id = (ann != null ? ann.id() : ""); this.listenerId = (!id.isEmpty() ? id : null); @@ -165,7 +166,9 @@ void init(ApplicationContext applicationContext, @Nullable EventExpressionEvalua @Override public void onApplicationEvent(ApplicationEvent event) { - processEvent(event); + if (isDefaultExecution()) { + processEvent(event); + } } @Override @@ -177,13 +180,14 @@ public boolean supportsEventType(ResolvableType eventType) { return true; } if (PayloadApplicationEvent.class.isAssignableFrom(eventType.toClass())) { - if (eventType.hasUnresolvableGenerics()) { - return true; - } ResolvableType payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric(); if (declaredEventType.isAssignableFrom(payloadType)) { return true; } + if (payloadType.resolve() == null) { + // Always accept such event when the type is erased + return true; + } } } return false; @@ -225,6 +229,16 @@ protected String getDefaultListenerId() { return ClassUtils.getQualifiedMethodName(method) + sj; } + /** + * Return whether default execution is applicable for the target listener. + * @since 6.2 + * @see #onApplicationEvent + * @see EventListener#defaultExecution() + */ + protected boolean isDefaultExecution() { + return this.defaultExecution; + } + /** * Process the specified {@link ApplicationEvent}, checking if the condition @@ -232,7 +246,7 @@ protected String getDefaultListenerId() { * @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) { @@ -254,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; } @@ -262,7 +277,7 @@ private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) { if (StringUtils.hasText(condition)) { Assert.notNull(this.evaluator, "EventExpressionEvaluator must not be null"); return this.evaluator.condition( - condition, event, this.targetMethod, this.methodKey, args, this.applicationContext); + condition, event, this.targetMethod, this.methodKey, args); } return true; } @@ -273,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; @@ -293,7 +307,6 @@ protected Object[] resolveArguments(ApplicationEvent event) { return new Object[] {event}; } - @SuppressWarnings({"deprecation", "unchecked"}) protected void handleResult(Object result) { if (reactiveStreamsPresent && new ReactiveResultHandler().subscribeToPublisher(result)) { if (logger.isTraceEnabled()) { @@ -310,16 +323,13 @@ else if (event != null) { } }); } - else if (result instanceof org.springframework.util.concurrent.ListenableFuture listenableFuture) { - listenableFuture.addCallback(this::publishEvents, this::handleAsyncError); - } else { publishEvents(result); } } - private void publishEvents(Object result) { - if (result.getClass().isArray()) { + private void publishEvents(@Nullable Object result) { + if (result != null && result.getClass().isArray()) { Object[] events = ObjectUtils.toObjectArray(result); for (Object event : events) { publishEvent(event); @@ -349,8 +359,7 @@ protected void handleAsyncError(Throwable t) { /** * Invoke the event listener method with the given argument values. */ - @Nullable - protected Object doInvoke(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)) { @@ -406,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; } @@ -416,8 +424,8 @@ protected String getCondition() { * the given error message. * @param message error message to append the HandlerMethod details to */ - protected String getDetailedErrorMessage(Object bean, String message) { - StringBuilder sb = new StringBuilder(message).append('\n'); + protected String getDetailedErrorMessage(Object bean, @Nullable String message) { + StringBuilder sb = (StringUtils.hasLength(message) ? new StringBuilder(message).append('\n') : new StringBuilder()); sb.append("HandlerMethod details: \n"); sb.append("Bean [").append(bean.getClass().getName()).append("]\n"); sb.append("Method [").append(this.method.toGenericString()).append("]\n"); @@ -431,19 +439,19 @@ protected String getDetailedErrorMessage(Object bean, String message) { * beans, and others). Event listener beans that require proxying should prefer * class-based proxy mechanisms. */ - private void assertTargetBean(Method method, Object targetBean, Object[] args) { + private void assertTargetBean(Method method, Object targetBean, @Nullable Object[] args) { Class methodDeclaringClass = method.getDeclaringClass(); Class targetBeanClass = targetBean.getClass(); if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { String msg = "The event listener method class '" + methodDeclaringClass.getName() + "' is not an instance of the actual bean class '" + targetBeanClass.getName() + "'. If the bean requires proxying " + - "(e.g. due to @Transactional), please use class-based proxying."; + "(for example, due to @Transactional), please use class-based proxying."; throw new IllegalStateException(getInvocationErrorMessage(targetBean, msg, args)); } } - private String getInvocationErrorMessage(Object bean, String message, 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++) { @@ -459,8 +467,7 @@ private String getInvocationErrorMessage(Object bean, String message, Object[] r 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/ContextClosedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java index 900bf30e49ca..8d0e2e56541c 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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,7 +29,7 @@ public class ContextClosedEvent extends ApplicationContextEvent { /** - * Creates a new ContextClosedEvent. + * Create a new {@code ContextClosedEvent}. * @param source the {@code ApplicationContext} that has been closed * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java index 27c657a948e6..ba55c6a56c27 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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,7 +29,7 @@ public class ContextRefreshedEvent extends ApplicationContextEvent { /** - * Create a new ContextRefreshedEvent. + * Create a new {@code ContextRefreshedEvent}. * @param source the {@code ApplicationContext} that has been initialized * or refreshed (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java index bfd615d5c120..f0cf6d6bb0d4 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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,7 +30,7 @@ public class ContextStartedEvent extends ApplicationContextEvent { /** - * Create a new ContextStartedEvent. + * Create a new {@code ContextStartedEvent}. * @param source the {@code ApplicationContext} that has been started * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java index 4a156b207b8c..791e08c282c2 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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,7 +30,7 @@ public class ContextStoppedEvent extends ApplicationContextEvent { /** - * Create a new ContextStoppedEvent. + * Create a new {@code ContextStoppedEvent}. * @param source the {@code ApplicationContext} that has been stopped * (must not be {@code null}) */ 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 ed1974f47a06..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -20,14 +20,15 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.beans.factory.BeanFactory; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEvent; import org.springframework.context.expression.AnnotatedElementKey; -import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.CachedExpressionEvaluator; import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; +import org.springframework.expression.spel.support.StandardEvaluationContext; /** * Utility class for handling SpEL expression parsing for application events. @@ -41,23 +42,32 @@ class EventExpressionEvaluator extends CachedExpressionEvaluator { private final Map conditionCache = new ConcurrentHashMap<>(64); + private final StandardEvaluationContext originalEvaluationContext; + + EventExpressionEvaluator(StandardEvaluationContext originalEvaluationContext) { + this.originalEvaluationContext = originalEvaluationContext; + } /** * Determine if the condition defined by the specified expression evaluates * to {@code true}. */ public boolean condition(String conditionExpression, ApplicationEvent event, Method targetMethod, - AnnotatedElementKey methodKey, Object[] args, @Nullable BeanFactory beanFactory) { - - EventExpressionRootObject root = new EventExpressionRootObject(event, args); - MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext( - root, targetMethod, args, getParameterNameDiscoverer()); - if (beanFactory != null) { - evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); - } + AnnotatedElementKey methodKey, @Nullable Object[] args) { + EventExpressionRootObject rootObject = new EventExpressionRootObject(event, args); + EvaluationContext evaluationContext = createEvaluationContext(rootObject, targetMethod, args); return (Boolean.TRUE.equals(getExpression(this.conditionCache, methodKey, conditionExpression).getValue( evaluationContext, Boolean.class))); } + private EvaluationContext createEvaluationContext(EventExpressionRootObject rootObject, + Method method, @Nullable Object[] args) { + + MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(rootObject, + method, args, getParameterNameDiscoverer()); + this.originalEvaluationContext.applyDelegatesTo(evaluationContext); + return evaluationContext; + } + } 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/EventListener.java b/spring-context/src/main/java/org/springframework/context/event/EventListener.java index 28ebecfa4ea6..8ee83d9e454e 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListener.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. @@ -132,9 +132,17 @@ */ String condition() default ""; + /** + * Whether the event should be handled by default, without any special + * pre-conditions such as an active transaction. Declared here for overriding + * in composed annotations such as {@code TransactionalEventListener}. + * @since 6.2 + */ + boolean defaultExecution() default true; + /** * An optional identifier for the listener, defaulting to the fully-qualified - * signature of the declaring method (e.g. "mypackage.MyClass.myMethod()"). + * signature of the declaring method (for example, "mypackage.MyClass.myMethod()"). * @since 5.3.5 * @see SmartApplicationListener#getListenerId() * @see ApplicationEventMulticaster#removeApplicationListeners(Predicate) 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 1fea6e198da0..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 @@ -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,7 +18,6 @@ import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -26,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; @@ -39,11 +39,12 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.MethodIntrospector; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -66,23 +67,22 @@ 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; - @Nullable - private final EventExpressionEvaluator evaluator; + private final StandardEvaluationContext originalEvaluationContext; - private final Set> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); + private final @Nullable EventExpressionEvaluator evaluator; + + private final Set> nonAnnotatedClasses = ConcurrentHashMap.newKeySet(64); public EventListenerMethodProcessor() { - this.evaluator = new EventExpressionEvaluator(); + this.originalEvaluationContext = new StandardEvaluationContext(); + this.evaluator = new EventExpressionEvaluator(this.originalEvaluationContext); } @Override @@ -95,6 +95,7 @@ public void setApplicationContext(ApplicationContext applicationContext) { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { this.beanFactory = beanFactory; + this.originalEvaluationContext.setBeanResolver(new BeanFactoryResolver(this.beanFactory)); Map beans = beanFactory.getBeansOfType(EventListenerFactory.class, false, false); List factories = new ArrayList<>(beans.values()); @@ -106,7 +107,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) @Override public void afterSingletonsInstantiated() { ConfigurableListableBeanFactory beanFactory = this.beanFactory; - Assert.state(this.beanFactory != null, "No ConfigurableListableBeanFactory set"); + Assert.state(beanFactory != null, "No ConfigurableListableBeanFactory set"); String[] beanNames = beanFactory.getBeanNamesForType(Object.class); for (String beanName : beanNames) { if (!ScopedProxyUtils.isScopedTarget(beanName)) { 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/GenericApplicationListener.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java index 763f96f533af..21e8b79da29a 100644 --- a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.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. @@ -16,6 +16,8 @@ package org.springframework.context.event; +import java.util.function.Consumer; + import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.ResolvableType; @@ -53,4 +55,17 @@ default boolean supportsEventType(Class eventType) { */ boolean supportsEventType(ResolvableType eventType); + + /** + * Create a new {@code ApplicationListener} for the given event type. + * @param eventType the event to listen to + * @param consumer the consumer to invoke when a matching event is fired + * @param the specific {@code ApplicationEvent} subclass to listen to + * @return a corresponding {@code ApplicationListener} instance + * @since 6.1.3 + */ + static GenericApplicationListener forEventType(Class eventType, Consumer consumer) { + return new GenericApplicationListenerDelegate<>(eventType, consumer); + } + } 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/GenericApplicationListenerDelegate.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerDelegate.java new file mode 100644 index 000000000000..9b2adac9c30e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerDelegate.java @@ -0,0 +1,54 @@ +/* + * 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.context.event; + +import java.util.function.Consumer; + +import org.springframework.context.ApplicationEvent; +import org.springframework.core.ResolvableType; + +/** + * A {@link GenericApplicationListener} implementation that supports a single event type. + * + * @author Stephane Nicoll + * @since 6.1.3 + * @param the specific {@code ApplicationEvent} subclass to listen to + */ +class GenericApplicationListenerDelegate implements GenericApplicationListener { + + private final Class supportedEventType; + + private final Consumer consumer; + + + GenericApplicationListenerDelegate(Class supportedEventType, Consumer consumer) { + this.supportedEventType = supportedEventType; + this.consumer = consumer; + } + + + @Override + public void onApplicationEvent(ApplicationEvent event) { + this.consumer.accept(this.supportedEventType.cast(event)); + } + + @Override + public boolean supportsEventType(ResolvableType eventType) { + return this.supportedEventType.isAssignableFrom(eventType.toClass()); + } + +} 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 ac7613f072d6..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 @@ -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. @@ -17,16 +17,17 @@ package org.springframework.context.event; import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; 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; /** @@ -50,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; /** @@ -85,7 +83,7 @@ public SimpleApplicationEventMulticaster(BeanFactory beanFactory) { * unless the TaskExecutor explicitly supports this. *

    {@link ApplicationListener} instances which declare no support for asynchronous * execution ({@link ApplicationListener#supportsAsyncExecution()} always run within - * the original thread which published the event, e.g. the transaction-synchronized + * the original thread which published the event, for example, the transaction-synchronized * {@link org.springframework.transaction.event.TransactionalApplicationListener}. * @since 2.0 * @see org.springframework.core.task.SyncTaskExecutor @@ -99,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; } @@ -116,7 +113,7 @@ protected Executor getTaskExecutor() { * and logs exceptions (a la * {@link org.springframework.scheduling.support.TaskUtils#LOG_AND_SUPPRESS_ERROR_HANDLER}) * or an implementation that logs exceptions while nevertheless propagating them - * (e.g. {@link org.springframework.scheduling.support.TaskUtils#LOG_AND_PROPAGATE_ERROR_HANDLER}). + * (for example, {@link org.springframework.scheduling.support.TaskUtils#LOG_AND_PROPAGATE_ERROR_HANDLER}). * @since 4.1 */ public void setErrorHandler(@Nullable ErrorHandler errorHandler) { @@ -127,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; } @@ -143,7 +139,13 @@ public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType even Executor executor = getTaskExecutor(); for (ApplicationListener listener : getApplicationListeners(event, type)) { if (executor != null && listener.supportsAsyncExecution()) { - executor.execute(() -> invokeListener(listener, event)); + try { + executor.execute(() -> invokeListener(listener, event)); + } + catch (RejectedExecutionException ex) { + // Probably on shutdown -> invoke listener locally instead + invokeListener(listener, event); + } } else { invokeListener(listener, event); 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 194d6ca133c0..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 @@ -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,13 +18,14 @@ 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; /** - * Represent an {@link AnnotatedElement} on a particular {@link Class} - * and is suitable as a key. + * Represents an {@link AnnotatedElement} in a particular {@link Class} + * and is suitable for use as a cache key. * * @author Costin Leau * @author Stephane Nicoll @@ -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 451d81b271c3..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 @@ -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,17 +16,18 @@ 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; /** - * EL property accessor that knows how to traverse the beans and contextual objects - * of a Spring {@link org.springframework.beans.factory.config.BeanExpressionContext}. + * SpEL {@link PropertyAccessor} that knows how to access the beans and contextual + * objects of a Spring {@link BeanExpressionContext}. * * @author Juergen Hoeller * @author Andy Clement @@ -34,6 +35,11 @@ */ public class BeanExpressionContextAccessor implements PropertyAccessor { + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {BeanExpressionContext.class}; + } + @Override public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { return (target instanceof BeanExpressionContext bec && bec.containsObject(name)); @@ -57,9 +63,4 @@ public void write(EvaluationContext context, @Nullable Object target, String nam throw new AccessException("Beans in a BeanFactory are read-only"); } - @Override - public Class[] getSpecificTargetClasses() { - return new Class[] {BeanExpressionContext.class}; - } - } 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 b8e90c56a7f1..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 @@ -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,17 +16,18 @@ 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; /** - * EL property accessor that knows how to traverse the beans of a - * Spring {@link org.springframework.beans.factory.BeanFactory}. + * SpEL {@link PropertyAccessor} that knows how to access the beans of a + * Spring {@link BeanFactory}. * * @author Juergen Hoeller * @author Andy Clement diff --git a/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryResolver.java b/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryResolver.java index 4dfaa20c60e9..e70819575b47 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryResolver.java +++ b/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,7 @@ import org.springframework.util.Assert; /** - * EL bean resolver that operates against a Spring - * {@link org.springframework.beans.factory.BeanFactory}. + * SpEL {@link BeanResolver} that operates against a Spring {@link BeanFactory}. * * @author Juergen Hoeller * @since 3.0.4 @@ -36,8 +35,8 @@ public class BeanFactoryResolver implements BeanResolver { /** - * Create a new {@link BeanFactoryResolver} for the given factory. - * @param beanFactory the {@link BeanFactory} to resolve bean names against + * Create a new {@code BeanFactoryResolver} for the given factory. + * @param beanFactory the {@code BeanFactory} to resolve bean names against */ public BeanFactoryResolver(BeanFactory beanFactory) { Assert.notNull(beanFactory, "BeanFactory must not be null"); 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 ffd9ff96984d..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 @@ -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,17 +18,17 @@ 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; -import org.springframework.util.ObjectUtils; /** * Shared utility class used to evaluate and cache SpEL expressions that - * are defined on {@link java.lang.reflect.AnnotatedElement}. + * are defined on an {@link java.lang.reflect.AnnotatedElement AnnotatedElement}. * * @author Stephane Nicoll * @since 4.2 @@ -42,18 +42,18 @@ public abstract class CachedExpressionEvaluator { /** - * Create a new instance with the specified {@link SpelExpressionParser}. + * Create a new instance with the default {@link SpelExpressionParser}. */ - protected CachedExpressionEvaluator(SpelExpressionParser parser) { - Assert.notNull(parser, "SpelExpressionParser must not be null"); - this.parser = parser; + protected CachedExpressionEvaluator() { + this(new SpelExpressionParser()); } /** - * Create a new instance with a default {@link SpelExpressionParser}. + * Create a new instance with the specified {@link SpelExpressionParser}. */ - protected CachedExpressionEvaluator() { - this(new SpelExpressionParser()); + protected CachedExpressionEvaluator(SpelExpressionParser parser) { + Assert.notNull(parser, "SpelExpressionParser must not be null"); + this.parser = parser; } @@ -72,24 +72,20 @@ protected ParameterNameDiscoverer getParameterNameDiscoverer() { return this.parameterNameDiscoverer; } - /** - * Return the {@link Expression} for the specified SpEL value - *

    {@link #parseExpression(String) Parse the expression} if it hasn't been already. + * Return the parsed {@link Expression} for the specified SpEL expression. + *

    {@linkplain #parseExpression(String) Parses} the expression if it hasn't + * already been parsed and cached. * @param cache the cache to use - * @param elementKey the element on which the expression is defined + * @param elementKey the {@code AnnotatedElementKey} containing the element + * on which the expression is defined * @param expression the expression to parse */ protected Expression getExpression(Map cache, AnnotatedElementKey elementKey, String expression) { ExpressionKey expressionKey = createKey(elementKey, expression); - Expression expr = cache.get(expressionKey); - if (expr == null) { - expr = parseExpression(expression); - cache.put(expressionKey, expr); - } - return expr; + return cache.computeIfAbsent(expressionKey, key -> parseExpression(expression)); } /** @@ -125,8 +121,7 @@ protected ExpressionKey(AnnotatedElementKey element, String expression) { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof ExpressionKey that && - this.element.equals(that.element) && - ObjectUtils.nullSafeEquals(this.expression, that.expression))); + this.element.equals(that.element) && this.expression.equals(that.expression))); } @Override 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 8e5dd9271939..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 @@ -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,16 +16,17 @@ 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; /** - * Read-only EL property accessor that knows how to retrieve keys + * Read-only SpEL {@link PropertyAccessor} that knows how to retrieve properties * of a Spring {@link Environment} instance. * * @author Chris Beams @@ -38,18 +39,14 @@ public Class[] getSpecificTargetClasses() { return new Class[] {Environment.class}; } - /** - * Can read any {@link Environment}, thus always returns true. - * @return true - */ @Override public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { - return true; + return (target instanceof Environment); } /** - * Access the given target object by resolving the given property name against the given target - * environment. + * Access the given target object by resolving the given property name against + * the given target environment. */ @Override public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { @@ -65,12 +62,11 @@ public boolean canWrite(EvaluationContext context, @Nullable Object target, Stri return false; } - /** - * Read-only: no-op. - */ @Override public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) throws AccessException { + + throw new AccessException("The Environment is read-only"); } } 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 e7374fe305fc..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 @@ -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. @@ -18,18 +18,20 @@ 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.PropertyAccessor; 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; /** - * EL property accessor that knows how to traverse the keys - * of a standard {@link java.util.Map}. + * SpEL {@link PropertyAccessor} that knows how to access the keys of a standard + * {@link java.util.Map}. * * @author Juergen Hoeller * @author Andy Clement @@ -37,6 +39,28 @@ */ public class MapAccessor implements CompilablePropertyAccessor { + private final boolean allowWrite; + + + /** + * Create a new {@code MapAccessor} for reading as well as writing. + * @see #MapAccessor(boolean) + */ + public MapAccessor() { + this(true); + } + + /** + * Create a new {@code MapAccessor} for reading and possibly also writing. + * @param allowWrite whether to allow write operations on a target instance + * @since 6.2 + * @see #canWrite + */ + public MapAccessor(boolean allowWrite) { + this.allowWrite = allowWrite; + } + + @Override public Class[] getSpecificTargetClasses() { return new Class[] {Map.class}; @@ -60,7 +84,7 @@ public TypedValue read(EvaluationContext context, @Nullable Object target, Strin @Override public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { - return true; + return (this.allowWrite && target instanceof Map); } @Override @@ -68,7 +92,7 @@ public boolean canWrite(EvaluationContext context, @Nullable Object target, Stri public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) throws AccessException { - Assert.state(target instanceof Map, "Target must be a Map"); + Assert.state(target instanceof Map, "Target must be of type Map"); Map map = (Map) target; map.put(name, newValue); } @@ -93,7 +117,7 @@ public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { CodeFlow.insertCheckCast(mv, "Ljava/util/Map"); } mv.visitLdcInsn(propertyName); - mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true); } 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 29bb92402fdf..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-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,12 +19,17 @@ 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; import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.SpringProperties; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParserContext; @@ -33,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; @@ -47,6 +51,7 @@ * beans such as "environment", "systemProperties" and "systemEnvironment". * * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 * @see BeanExpressionContext#getBeanFactory() * @see org.springframework.expression.ExpressionParser @@ -55,6 +60,14 @@ */ public class StandardBeanExpressionResolver implements BeanExpressionResolver { + /** + * System property to configure the maximum length for SpEL expressions: {@value}. + *

    Can also be configured via the {@link SpringProperties} mechanism. + * @since 6.1.3 + * @see SpelParserConfiguration#getMaximumExpressionLength() + */ + public static final String MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME = "spring.context.expression.maxLength"; + /** Default expression prefix: "#{". */ public static final String DEFAULT_EXPRESSION_PREFIX = "#{"; @@ -90,18 +103,24 @@ public String getExpressionSuffix() { /** * Create a new {@code StandardBeanExpressionResolver} with default settings. + *

    As of Spring Framework 6.1.3, the maximum SpEL expression length can be + * configured via the {@link #MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME} property. */ public StandardBeanExpressionResolver() { - this.expressionParser = new SpelExpressionParser(); + this(null); } /** * Create a new {@code StandardBeanExpressionResolver} with the given bean class loader, * using it as the basis for expression compilation. + *

    As of Spring Framework 6.1.3, the maximum SpEL expression length can be + * configured via the {@link #MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME} property. * @param beanClassLoader the factory's bean class loader */ public StandardBeanExpressionResolver(@Nullable ClassLoader beanClassLoader) { - this.expressionParser = new SpelExpressionParser(new SpelParserConfiguration(null, beanClassLoader)); + SpelParserConfiguration parserConfig = new SpelParserConfiguration( + null, beanClassLoader, false, false, Integer.MAX_VALUE, retrieveMaxExpressionLength()); + this.expressionParser = new SpelExpressionParser(parserConfig); } @@ -137,34 +156,30 @@ 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; } try { - Expression expr = this.expressionCache.get(value); - if (expr == null) { - expr = this.expressionParser.parseExpression(value, this.beanExpressionParserContext); - this.expressionCache.put(value, expr); - } - StandardEvaluationContext sec = this.evaluationCache.get(beanExpressionContext); - if (sec == null) { - sec = new StandardEvaluationContext(beanExpressionContext); - sec.addPropertyAccessor(new BeanExpressionContextAccessor()); - sec.addPropertyAccessor(new BeanFactoryAccessor()); - sec.addPropertyAccessor(new MapAccessor()); - sec.addPropertyAccessor(new EnvironmentAccessor()); - sec.setBeanResolver(new BeanFactoryResolver(beanExpressionContext.getBeanFactory())); - sec.setTypeLocator(new StandardTypeLocator(beanExpressionContext.getBeanFactory().getBeanClassLoader())); - sec.setTypeConverter(new StandardTypeConverter(() -> { - ConversionService cs = beanExpressionContext.getBeanFactory().getConversionService(); - return (cs != null ? cs : DefaultConversionService.getSharedInstance()); - })); - customizeEvaluationContext(sec); - this.evaluationCache.put(beanExpressionContext, sec); - } - return expr.getValue(sec); + Expression expr = this.expressionCache.computeIfAbsent(value, expression -> + this.expressionParser.parseExpression(expression, this.beanExpressionParserContext)); + EvaluationContext evalContext = this.evaluationCache.computeIfAbsent(beanExpressionContext, bec -> { + ConfigurableBeanFactory beanFactory = bec.getBeanFactory(); + StandardEvaluationContext sec = new StandardEvaluationContext(bec); + sec.addPropertyAccessor(new BeanExpressionContextAccessor()); + sec.addPropertyAccessor(new BeanFactoryAccessor()); + sec.addPropertyAccessor(new MapAccessor()); + sec.addPropertyAccessor(new EnvironmentAccessor()); + sec.setBeanResolver(new BeanFactoryResolver(beanFactory)); + sec.setTypeLocator(new StandardTypeLocator(beanFactory.getBeanClassLoader())); + sec.setTypeConverter(new StandardTypeConverter(() -> { + ConversionService cs = beanFactory.getConversionService(); + return (cs != null ? cs : DefaultConversionService.getSharedInstance()); + })); + customizeEvaluationContext(sec); + return sec; + }); + return expr.getValue(evalContext); } catch (Throwable ex) { throw new BeanExpressionException("Expression parsing failed", ex); @@ -178,4 +193,22 @@ public Object evaluate(@Nullable String value, BeanExpressionContext beanExpress protected void customizeEvaluationContext(StandardEvaluationContext evalContext) { } + private static int retrieveMaxExpressionLength() { + String value = SpringProperties.getProperty(MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME); + if (!StringUtils.hasText(value)) { + return SpelParserConfiguration.DEFAULT_MAX_EXPRESSION_LENGTH; + } + + 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"); + return maxLength; + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("Failed to parse value for system property [" + + MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME + "]: " + ex.getMessage(), 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 new file mode 100644 index 000000000000..769a14ad4cc0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java @@ -0,0 +1,58 @@ +/* + * 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.context.i18n; + +import io.micrometer.context.ThreadLocalAccessor; +import org.jspecify.annotations.Nullable; + +/** + * Adapt {@link LocaleContextHolder} to the {@link ThreadLocalAccessor} contract + * to assist the Micrometer Context Propagation library with {@link LocaleContext} + * propagation. + * + * @author Tadaya Tsuyukubo + * @since 6.2 + */ +public class LocaleContextThreadLocalAccessor implements ThreadLocalAccessor { + + /** + * Key under which this accessor is registered in + * {@link io.micrometer.context.ContextRegistry}. + */ + public static final String KEY = LocaleContextThreadLocalAccessor.class.getName() + ".KEY"; + + @Override + public Object key() { + return KEY; + } + + @Override + public @Nullable LocaleContext getValue() { + return LocaleContextHolder.getLocaleContext(); + } + + @Override + public void setValue(LocaleContext value) { + LocaleContextHolder.setLocaleContext(value); + } + + @Override + public void setValue() { + LocaleContextHolder.resetLocaleContext(); + } + +} 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 071ea63d9455..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 @@ -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,14 +26,20 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; 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; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryInitializer; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; @@ -81,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; @@ -113,7 +118,7 @@ * {@link org.springframework.core.io.DefaultResourceLoader}. * Consequently treats non-URL resource paths as class path resources * (supporting full class path resource names that include the package path, - * e.g. "mypackage/myresource.dat"), unless the {@link #getResourceByPath} + * for example, "mypackage/myresource.dat"), unless the {@link #getResourceByPath} * method is overridden in a subclass. * * @author Rod Johnson @@ -135,17 +140,6 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { - /** - * The name of the {@link LifecycleProcessor} bean in the context. - * If none is supplied, a {@link DefaultLifecycleProcessor} is used. - * @since 3.0 - * @see org.springframework.context.LifecycleProcessor - * @see org.springframework.context.support.DefaultLifecycleProcessor - * @see #start() - * @see #stop() - */ - public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; - /** * The name of the {@link MessageSource} bean in the context. * If none is supplied, message resolution is delegated to the parent. @@ -166,6 +160,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader */ public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster"; + /** + * The name of the {@link LifecycleProcessor} bean in the context. + * If none is supplied, a {@link DefaultLifecycleProcessor} is used. + * @since 3.0 + * @see org.springframework.context.LifecycleProcessor + * @see org.springframework.context.support.DefaultLifecycleProcessor + * @see #start() + * @see #stop() + */ + public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; + static { // Eagerly load the ContextClosedEvent class to avoid weird classloader issues @@ -184,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<>(); @@ -203,41 +206,38 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader /** Flag that indicates whether this context has been closed already. */ private final AtomicBoolean closed = new AtomicBoolean(); - /** Synchronization monitor for the "refresh" and "destroy". */ - private final Object startupShutdownMonitor = new Object(); + /** Synchronization lock for "refresh" and "close". */ + private final Lock startupShutdownLock = new ReentrantLock(); + + /** Currently active startup/shutdown thread. */ + 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. **/ + /** Application startup metrics. */ private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; /** Statically specified listeners. */ 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; /** @@ -306,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; } @@ -576,7 +575,10 @@ public Collection> getApplicationListeners() { @Override public void refresh() throws BeansException, IllegalStateException { - synchronized (this.startupShutdownMonitor) { + this.startupShutdownLock.lock(); + try { + this.startupShutdownThread = Thread.currentThread(); + StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh"); // Prepare this context for refreshing. @@ -595,7 +597,6 @@ public void refresh() throws BeansException, IllegalStateException { StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process"); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); - // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); beanPostProcess.end(); @@ -624,6 +625,7 @@ public void refresh() throws BeansException, IllegalStateException { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } + // Destroy already created singletons to avoid dangling resources. destroyBeans(); @@ -638,6 +640,10 @@ public void refresh() throws BeansException, IllegalStateException { contextRefresh.end(); } } + finally { + this.startupShutdownThread = null; + this.startupShutdownLock.unlock(); + } } /** @@ -775,7 +781,7 @@ protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory b PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); // Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime - // (e.g. through an @Bean method registered by ConfigurationClassPostProcessor) + // (for example, through an @Bean method registered by ConfigurationClassPostProcessor) if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); @@ -793,8 +799,9 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa } /** - * Initialize the MessageSource. - * Use parent's if none defined in this context. + * Initialize the {@link MessageSource}. + *

    Uses parent's {@code MessageSource} if none defined in this context. + * @see #MESSAGE_SOURCE_BEAN_NAME */ protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); @@ -824,8 +831,9 @@ protected void initMessageSource() { } /** - * Initialize the ApplicationEventMulticaster. - * Uses SimpleApplicationEventMulticaster if none defined in the context. + * Initialize the {@link ApplicationEventMulticaster}. + *

    Uses {@link SimpleApplicationEventMulticaster} if none defined in the context. + * @see #APPLICATION_EVENT_MULTICASTER_BEAN_NAME * @see org.springframework.context.event.SimpleApplicationEventMulticaster */ protected void initApplicationEventMulticaster() { @@ -848,15 +856,16 @@ protected void initApplicationEventMulticaster() { } /** - * Initialize the LifecycleProcessor. - * Uses DefaultLifecycleProcessor if none defined in the context. + * Initialize the {@link LifecycleProcessor}. + *

    Uses {@link DefaultLifecycleProcessor} if none defined in the context. + * @since 3.0 + * @see #LIFECYCLE_PROCESSOR_BEAN_NAME * @see org.springframework.context.support.DefaultLifecycleProcessor */ protected void initLifecycleProcessor() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) { - this.lifecycleProcessor = - beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); + this.lifecycleProcessor = beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); if (logger.isTraceEnabled()) { logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]"); } @@ -915,7 +924,15 @@ protected void registerListeners() { * Finish the initialization of this context's bean factory, * initializing all remaining singleton beans. */ + @SuppressWarnings("unchecked") protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Initialize bootstrap executor for this context. + if (beanFactory.containsBean(BOOTSTRAP_EXECUTOR_BEAN_NAME) && + beanFactory.isTypeMatch(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)) { + beanFactory.setBootstrapExecutor( + beanFactory.getBean(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)); + } + // Initialize conversion service for this context. if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) && beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { @@ -930,10 +947,24 @@ protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory b beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal)); } + // Call BeanFactoryInitializer beans early to allow for initializing specific other beans early. + String[] initializerNames = beanFactory.getBeanNamesForType(BeanFactoryInitializer.class, false, false); + for (String initializerName : initializerNames) { + beanFactory.getBean(initializerName, BeanFactoryInitializer.class).initialize(beanFactory); + } + // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); for (String weaverAwareName : weaverAwareNames) { - getBean(weaverAwareName); + try { + beanFactory.getBean(weaverAwareName, LoadTimeWeaverAware.class); + } + catch (BeanNotOfRequiredTypeException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to initialize LoadTimeWeaverAware bean '" + weaverAwareName + + "' due to unexpected type mismatch: " + ex.getMessage()); + } + } } // Stop using the temporary ClassLoader for type matching. @@ -997,6 +1028,14 @@ protected void resetCommonCaches() { CachedIntrospectionResults.clearClassLoader(getClassLoader()); } + @Override + public void clearResourceCaches() { + super.clearResourceCaches(); + if (this.resourcePatternResolver instanceof PathMatchingResourcePatternResolver pmrpr) { + pmrpr.clearCache(); + } + } + /** * Register a shutdown hook {@linkplain Thread#getName() named} @@ -1015,15 +1054,47 @@ public void registerShutdownHook() { this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) { @Override public void run() { - synchronized (startupShutdownMonitor) { + if (isStartupShutdownThreadStuck()) { + active.set(false); + return; + } + startupShutdownLock.lock(); + try { doClose(); } + finally { + startupShutdownLock.unlock(); + } } }; Runtime.getRuntime().addShutdownHook(this.shutdownHook); } } + /** + * Determine whether an active startup/shutdown thread is currently stuck, + * for example, through a {@code System.exit} call in a user component. + */ + private boolean isStartupShutdownThreadStuck() { + Thread activeThread = this.startupShutdownThread; + if (activeThread != null && activeThread.getState() == Thread.State.WAITING) { + // Indefinitely waiting: might be Thread.join or the like, or System.exit + activeThread.interrupt(); + try { + // Leave just a little bit of time for the interruption to show effect + Thread.sleep(1); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + if (activeThread.getState() == Thread.State.WAITING) { + // Interrupted but still waiting: very likely a System.exit call + return true; + } + } + return false; + } + /** * Close this application context, destroying all beans in its bean factory. *

    Delegates to {@code doClose()} for the actual closing procedure. @@ -1033,8 +1104,17 @@ public void run() { */ @Override public void close() { - synchronized (this.startupShutdownMonitor) { + if (isStartupShutdownThreadStuck()) { + this.active.set(false); + return; + } + + this.startupShutdownLock.lock(); + try { + this.startupShutdownThread = Thread.currentThread(); + doClose(); + // If we registered a JVM shutdown hook, we don't need it anymore now: // We've already explicitly closed the context. if (this.shutdownHook != null) { @@ -1046,6 +1126,10 @@ public void close() { } } } + finally { + this.startupShutdownThread = null; + this.startupShutdownLock.unlock(); + } } /** @@ -1137,6 +1221,11 @@ protected void onClose() { // For subclasses: do nothing by default. } + @Override + public boolean isClosed() { + return this.closed.get(); + } + @Override public boolean isActive() { return this.active.get(); @@ -1180,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); } @@ -1192,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); } @@ -1239,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); } @@ -1342,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(); @@ -1351,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 { @@ -1375,8 +1460,7 @@ public Set findAllAnnotationsOnBean( //--------------------------------------------------------------------- @Override - @Nullable - public BeanFactory getParentBeanFactory() { + public @Nullable BeanFactory getParentBeanFactory() { return getParent(); } @@ -1390,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()); } @@ -1402,12 +1485,12 @@ protected BeanFactory getInternalParentBeanFactory() { //--------------------------------------------------------------------- @Override - 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); } @@ -1433,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 d58ceadb5132..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,15 +78,14 @@ public void setParentMessageSource(@Nullable MessageSource parent) { } @Override - @Nullable - public MessageSource getParentMessageSource() { + public @Nullable MessageSource getParentMessageSource() { return this.parentMessageSource; } /** * Specify locale-independent common messages, with the message code as key * and the full message String (may contain argument placeholders) as value. - *

    May also link to an externally defined Properties object, e.g. defined + *

    May also link to an externally defined Properties object, for example, defined * through a {@link org.springframework.beans.factory.config.PropertiesFactoryBean}. */ public void setCommonMessages(@Nullable Properties commonMessages) { @@ -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,7 +134,7 @@ protected boolean isUseCodeAsDefaultMessage() { @Override - 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; @@ -149,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; @@ -194,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; } @@ -251,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) { @@ -282,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) { @@ -312,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; } @@ -330,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); } @@ -363,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) { @@ -387,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 fd1bc8e12ff4..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 @@ -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,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; /** @@ -126,6 +124,7 @@ protected final void refreshBeanFactory() throws BeansException { try { DefaultListableBeanFactory beanFactory = createBeanFactory(); beanFactory.setSerializationId(getId()); + beanFactory.setApplicationStartup(getApplicationStartup()); customizeBeanFactory(beanFactory); loadBeanDefinitions(beanFactory); this.beanFactory = 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 37ffa85e40fd..45dafd462d11 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-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. @@ -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; @@ -54,7 +53,7 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage * Set a single basename, following the basic ResourceBundle convention * of not specifying file extension or language codes. The resource location * format is up to the specific {@code MessageSource} implementation. - *

    Regular and XMl properties files are supported: e.g. "messages" will find + *

    Regular and XMl properties files are supported: for example, "messages" will find * a "messages.properties", "messages_en.properties" etc arrangement as well * as "messages.xml", "messages_en.xml" etc. * @param basename the single basename @@ -70,7 +69,7 @@ public void setBasename(String basename) { * Set an array of basenames, each following the basic ResourceBundle convention * of not specifying file extension or language codes. The resource location * format is up to the specific {@code MessageSource} implementation. - *

    Regular and XMl properties files are supported: e.g. "messages" will find + *

    Regular and XMl properties files are supported: for example, "messages" will find * a "messages.properties", "messages_en.properties" etc arrangement as well * as "messages.xml", "messages_en.xml" etc. *

    The associated resource bundles will be checked sequentially when resolving @@ -110,6 +109,7 @@ public void addBasenames(String... basenames) { * in the order of registration. *

    Calling code may introspect this set as well as add or remove entries. * @since 4.3 + * @see #setBasenames * @see #addBasenames */ public Set getBasenameSet() { @@ -133,15 +133,14 @@ 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; } /** * Set whether to fall back to the system Locale if no files for a specific * Locale have been found. Default is "true"; if this is turned off, the only - * fallback will be the default file (e.g. "messages.properties" for + * fallback will be the default file (for example, "messages.properties" for * basename "messages"). *

    Falling back to the system Locale is the default behavior of * {@code java.util.ResourceBundle}. However, this is often not desirable @@ -186,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 c21002abd2cc..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} @@ -98,7 +99,7 @@ protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throw /** * Initialize the bean definition reader used for loading the bean definitions * of this context. The default implementation sets the validating flag. - *

    Can be overridden in subclasses, e.g. for turning off XML validation + *

    Can be overridden in subclasses, for example, for turning off XML validation * or using a different {@link BeanDefinitionDocumentReader} implementation. * @param reader the bean definition reader used by this context * @see XmlBeanDefinitionReader#setValidating @@ -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 df003f083fd5..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 @@ -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,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,16 +30,18 @@ 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; /** - * {@link BeanPostProcessor} implementation that supplies the {@code ApplicationContext}, - * {@link org.springframework.core.env.Environment Environment}, or - * {@link StringValueResolver} for the {@code ApplicationContext} to beans that - * implement the {@link EnvironmentAware}, {@link EmbeddedValueResolverAware}, - * {@link ResourceLoaderAware}, {@link ApplicationEventPublisherAware}, - * {@link MessageSourceAware}, and/or {@link ApplicationContextAware} interfaces. + * {@link BeanPostProcessor} implementation that supplies the + * {@link org.springframework.context.ApplicationContext ApplicationContext}, + * {@link org.springframework.core.env.Environment Environment}, + * {@link StringValueResolver}, or + * {@link org.springframework.core.metrics.ApplicationStartup ApplicationStartup} + * for the {@code ApplicationContext} to beans that implement the {@link EnvironmentAware}, + * {@link EmbeddedValueResolverAware}, {@link ResourceLoaderAware}, + * {@link ApplicationEventPublisherAware}, {@link MessageSourceAware}, + * {@link ApplicationStartupAware}, and/or {@link ApplicationContextAware} interfaces. * *

    Implemented interfaces are satisfied in the order in which they are * mentioned above. @@ -55,6 +59,7 @@ * @see org.springframework.context.ResourceLoaderAware * @see org.springframework.context.ApplicationEventPublisherAware * @see org.springframework.context.MessageSourceAware + * @see org.springframework.context.ApplicationStartupAware * @see org.springframework.context.ApplicationContextAware * @see org.springframework.context.support.AbstractApplicationContext#refresh() */ @@ -75,42 +80,34 @@ public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicati @Override - @Nullable - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (!(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware || - bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware || - bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware || - bean instanceof ApplicationStartupAware)) { - return bean; + public @Nullable Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof Aware) { + invokeAwareInterfaces(bean); } - - invokeAwareInterfaces(bean); return bean; } private void invokeAwareInterfaces(Object bean) { - if (bean instanceof Aware) { - if (bean instanceof EnvironmentAware environmentAware) { - environmentAware.setEnvironment(this.applicationContext.getEnvironment()); - } - if (bean instanceof EmbeddedValueResolverAware embeddedValueResolverAware) { - embeddedValueResolverAware.setEmbeddedValueResolver(this.embeddedValueResolver); - } - if (bean instanceof ResourceLoaderAware resourceLoaderAware) { - resourceLoaderAware.setResourceLoader(this.applicationContext); - } - if (bean instanceof ApplicationEventPublisherAware applicationEventPublisherAware) { - applicationEventPublisherAware.setApplicationEventPublisher(this.applicationContext); - } - if (bean instanceof MessageSourceAware messageSourceAware) { - messageSourceAware.setMessageSource(this.applicationContext); - } - if (bean instanceof ApplicationStartupAware applicationStartupAware) { - applicationStartupAware.setApplicationStartup(this.applicationContext.getApplicationStartup()); - } - if (bean instanceof ApplicationContextAware applicationContextAware) { - applicationContextAware.setApplicationContext(this.applicationContext); - } + if (bean instanceof EnvironmentAware environmentAware) { + environmentAware.setEnvironment(this.applicationContext.getEnvironment()); + } + if (bean instanceof EmbeddedValueResolverAware embeddedValueResolverAware) { + embeddedValueResolverAware.setEmbeddedValueResolver(this.embeddedValueResolver); + } + if (bean instanceof ResourceLoaderAware resourceLoaderAware) { + resourceLoaderAware.setResourceLoader(this.applicationContext); + } + if (bean instanceof ApplicationEventPublisherAware applicationEventPublisherAware) { + applicationEventPublisherAware.setApplicationEventPublisher(this.applicationContext); + } + if (bean instanceof MessageSourceAware messageSourceAware) { + messageSourceAware.setMessageSource(this.applicationContext); + } + if (bean instanceof ApplicationStartupAware applicationStartupAware) { + applicationStartupAware.setApplicationStartup(this.applicationContext.getApplicationStartup()); + } + if (bean instanceof ApplicationContextAware applicationContextAware) { + applicationContextAware.setApplicationContext(this.applicationContext); } } 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 9e298434699e..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,23 +18,23 @@ 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; /** * Convenient superclass for application objects that want to be aware of - * the application context, e.g. for custom lookup of collaborating beans + * the application context, for example, for custom lookup of collaborating beans * or for context-specific resource access. It saves the application * context reference and provides an initialization callback method. * Furthermore, it offers numerous convenience methods for message lookup. * *

    There is no requirement to subclass this class: It just makes things - * a little easier if you need access to the context, e.g. for access to + * a little easier if you need access to the context, for example, for access to * file resources or to the message source. Note that many application * objects do not need to be aware of the application context at all, * as they can receive collaborating beans via bean references. @@ -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 9b72875faa8d..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,17 +16,18 @@ 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; /** * Standalone XML application context, taking the context definition files * from the class path, interpreting plain paths as class path resource names - * that include the package path (e.g. "mypackage/myresource.txt"). Useful for + * that include the package path (for example, "mypackage/myresource.txt"). Useful for * test harnesses as well as for application contexts embedded within JARs. * *

    The config location defaults can be overridden via {@link #getConfigLocations}, @@ -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 998d061ca2cd..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,7 +122,7 @@ protected boolean isEligibleForOverriding(String className) { } @Override - 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 78c9db048332..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-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,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,62 +53,84 @@ 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; /** - * Default implementation of the {@link LifecycleProcessor} strategy. + * Spring's default implementation of the {@link LifecycleProcessor} strategy. * *

    Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in * groups for specific phases, on startup/shutdown as well as for explicit start/stop * 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 { /** * Property name for a common context checkpoint: {@value}. * @since 6.1 - * @see #CHECKPOINT_ON_REFRESH_VALUE + * @see #ON_REFRESH_VALUE * @see org.crac.Core#checkpointRestore() */ public static final String CHECKPOINT_PROPERTY_NAME = "spring.context.checkpoint"; /** - * Recognized value for the context checkpoint property: {@value}. + * Property name for terminating the JVM when the context reaches a specific phase: {@value}. + * @since 6.1 + * @see #ON_REFRESH_VALUE + */ + public static final String EXIT_PROPERTY_NAME = "spring.context.exit"; + + /** + * Recognized value for the context checkpoint and exit properties: {@value}. * @since 6.1 * @see #CHECKPOINT_PROPERTY_NAME - * @see org.crac.Core#checkpointRestore() + * @see #EXIT_PROPERTY_NAME */ - public static final String CHECKPOINT_ON_REFRESH_VALUE = "onRefresh"; + public static final String ON_REFRESH_VALUE = "onRefresh"; - private static final boolean checkpointOnRefresh = - CHECKPOINT_ON_REFRESH_VALUE.equalsIgnoreCase(SpringProperties.getProperty(CHECKPOINT_PROPERTY_NAME)); + private static boolean checkpointOnRefresh = + ON_REFRESH_VALUE.equalsIgnoreCase(SpringProperties.getProperty(CHECKPOINT_PROPERTY_NAME)); + + private static final boolean exitOnRefresh = + ON_REFRESH_VALUE.equalsIgnoreCase(SpringProperties.getProperty(EXIT_PROPERTY_NAME)); private final Log logger = LogFactory.getLog(getClass()); - private volatile long timeoutPerShutdownPhase = 30000; + 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() { @@ -118,10 +144,80 @@ 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 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 phasesWithTimeouts) { + this.timeoutsForShutdownPhases.putAll(phasesWithTimeouts); + } + + /** + * Specify the maximum time allotted for the shutdown of a specific 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 phase the phase value (matching {@link SmartLifecycle#getPhase()}) + * @param timeout the corresponding timeout value (in milliseconds) + * @since 6.2 + * @see SmartLifecycle#getPhase() + * @see #setTimeoutPerShutdownPhase + */ + public void setTimeoutForShutdownPhase(int phase, long timeout) { + this.timeoutsForShutdownPhases.put(phase, timeout); + } + /** * Specify the maximum time allotted in milliseconds for the shutdown of any * phase (group of {@link SmartLifecycle} beans with the same 'phase' value). - *

    The default value is 30000 milliseconds (30 seconds). + *

    The default value is 10000 milliseconds (10 seconds) as of 6.2. * @see SmartLifecycle#getPhase() */ public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) { @@ -134,6 +230,9 @@ public void setBeanFactory(BeanFactory beanFactory) { 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; } @@ -143,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 @@ -180,8 +294,12 @@ public void stop() { @Override public void onRefresh() { if (checkpointOnRefresh) { + checkpointOnRefresh = false; new CracDelegate().checkpointRestore(); } + if (exitOnRefresh) { + Runtime.getRuntime().halt(0); + } this.stoppedBeans = null; try { @@ -212,7 +330,7 @@ public boolean isRunning() { void stopForRestart() { if (this.running) { - this.stoppedBeans = Collections.newSetFromMap(new ConcurrentHashMap<>()); + this.stoppedBeans = ConcurrentHashMap.newKeySet(); stopBeans(); this.running = false; } @@ -232,13 +350,12 @@ private void startBeans(boolean autoStartupOnly) { lifecycleBeans.forEach((beanName, bean) -> { if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) { - int phase = getPhase(bean); - phases.computeIfAbsent( - phase, - p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) - ).add(beanName, bean); + int startupPhase = getPhase(bean); + phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly)) + .add(beanName, bean); } }); + if (!phases.isEmpty()) { phases.values().forEach(LifecycleGroup::start); } @@ -256,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() + "]"); + if (futures != null) { + futures.add(CompletableFuture.runAsync(() -> doStart(beanName, bean), getBootstrapExecutor())); } - try { - bean.start(); - } - catch (Throwable ex) { - throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex); - } - if (logger.isDebugEnabled()) { - logger.debug("Successfully started bean '" + beanName + "'"); + else { + doStart(beanName, bean); } } } } + private void doStart(String beanName, Lifecycle bean) { + if (logger.isTraceEnabled()) { + logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]"); + } + try { + bean.start(); + } + catch (Throwable ex) { + throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex); + } + if (logger.isDebugEnabled()) { + logger.debug("Successfully started bean '" + beanName + "'"); + } + } + private boolean toBeStarted(String beanName, Lifecycle bean) { Set stoppedBeans = this.stoppedBeans; return (stoppedBeans != null ? stoppedBeans.contains(beanName) : @@ -289,13 +417,13 @@ private boolean toBeStarted(String beanName, Lifecycle bean) { private void stopBeans() { Map lifecycleBeans = getLifecycleBeans(); Map phases = new TreeMap<>(Comparator.reverseOrder()); + lifecycleBeans.forEach((beanName, bean) -> { int shutdownPhase = getPhase(bean); - phases.computeIfAbsent( - shutdownPhase, - p -> new LifecycleGroup(shutdownPhase, this.timeoutPerShutdownPhase, lifecycleBeans, false) - ).add(beanName, bean); + phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false)) + .add(beanName, bean); }); + if (!phases.isEmpty()) { phases.values().forEach(LifecycleGroup::stop); } @@ -356,12 +484,15 @@ else if (bean instanceof SmartLifecycle) { if (logger.isWarnEnabled()) { logger.warn("Failed to stop bean '" + beanName + "'", ex); } + if (bean instanceof SmartLifecycle) { + latch.countDown(); + } } } } - // overridable hooks + // Overridable hooks /** * Retrieve all applicable Lifecycle beans: all singletons that have already been created, @@ -417,8 +548,6 @@ private class LifecycleGroup { private final int phase; - private final long timeout; - private final Map lifecycleBeans; private final boolean autoStartupOnly; @@ -427,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; } @@ -450,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); + } } } @@ -475,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("Failed to shut down " + countDownBeanNames.size() + " bean" + - (countDownBeanNames.size() > 1 ? "s" : "") + " with phase value " + - this.phase + " within 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) { @@ -536,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 e6abe2b0f121..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,16 +16,17 @@ 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 * from the file system or from URLs, interpreting plain paths as relative - * file system locations (e.g. "mydir/myfile.txt"). Useful for test harnesses + * file system locations (for example, "mydir/myfile.txt"). Useful for test harnesses * as well as for standalone environments. * *

    NOTE: Plain paths will always be interpreted as relative 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 55047b4d03b3..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-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,15 +18,19 @@ import java.io.IOException; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.List; 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; @@ -35,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; @@ -44,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; /** @@ -106,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; @@ -269,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(); } @@ -424,20 +426,53 @@ public void refreshForAotProcessing(RuntimeHints runtimeHints) { * @see SmartInstantiationAwareBeanPostProcessor#determineBeanType */ private void preDetermineBeanTypes(RuntimeHints runtimeHints) { + List singletons = new ArrayList<>(); + List lazyBeans = new ArrayList<>(); + + // First round: pre-registered singleton instances, if any. + for (String beanName : this.beanFactory.getSingletonNames()) { + Class beanType = this.beanFactory.getType(beanName); + if (beanType != null) { + ClassHintUtils.registerProxyIfNecessary(beanType, runtimeHints); + } + singletons.add(beanName); + } + List bpps = PostProcessorRegistrationDelegate.loadBeanPostProcessors( this.beanFactory, SmartInstantiationAwareBeanPostProcessor.class); + // Second round: non-lazy singleton beans in definition order, + // matching preInstantiateSingletons. for (String beanName : this.beanFactory.getBeanDefinitionNames()) { - Class beanType = this.beanFactory.getType(beanName); - if (beanType != null) { - ClassHintUtils.registerProxyIfNecessary(beanType, runtimeHints); - for (SmartInstantiationAwareBeanPostProcessor bpp : bpps) { - Class newBeanType = bpp.determineBeanType(beanType, beanName); - if (newBeanType != beanType) { - ClassHintUtils.registerProxyIfNecessary(newBeanType, runtimeHints); - beanType = newBeanType; - } + if (!singletons.contains(beanName)) { + BeanDefinition bd = getBeanDefinition(beanName); + if (bd.isSingleton() && !bd.isLazyInit()) { + preDetermineBeanType(beanName, bpps, runtimeHints); + } + else { + lazyBeans.add(beanName); + } + } + } + + // Third round: lazy singleton beans and scoped beans. + for (String beanName : lazyBeans) { + preDetermineBeanType(beanName, bpps, runtimeHints); + } + } + + private void preDetermineBeanType(String beanName, List bpps, + RuntimeHints runtimeHints) { + + Class beanType = this.beanFactory.getType(beanName); + if (beanType != null) { + ClassHintUtils.registerProxyIfNecessary(beanType, runtimeHints); + for (SmartInstantiationAwareBeanPostProcessor bpp : bpps) { + Class newBeanType = bpp.determineBeanType(beanType, beanName); + if (newBeanType != beanType) { + ClassHintUtils.registerProxyIfNecessary(newBeanType, runtimeHints); + beanType = newBeanType; } } } @@ -458,7 +493,7 @@ private void preDetermineBeanTypes(RuntimeHints runtimeHints) { * (may be {@code null} or empty) * @since 5.2 (since 5.0 on the AnnotationConfigApplicationContext subclass) */ - public void registerBean(Class beanClass, Object... constructorArgs) { + public void registerBean(Class beanClass, @Nullable Object... constructorArgs) { registerBean(null, beanClass, constructorArgs); } @@ -473,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) { @@ -488,7 +523,7 @@ public void registerBean(@Nullable String beanName, Class beanClass, Obje * @param beanClass the class of the bean (resolving a public constructor * to be autowired, possibly simply the default constructor) * @param customizers one or more callbacks for customizing the factory's - * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * {@link BeanDefinition}, for example, setting a lazy-init or primary flag * @since 5.0 * @see #registerBean(String, Class, Supplier, BeanDefinitionCustomizer...) */ @@ -503,7 +538,7 @@ public final void registerBean(Class beanClass, BeanDefinitionCustomizer. * @param beanClass the class of the bean (resolving a public constructor * to be autowired, possibly simply the default constructor) * @param customizers one or more callbacks for customizing the factory's - * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * {@link BeanDefinition}, for example, setting a lazy-init or primary flag * @since 5.0 * @see #registerBean(String, Class, Supplier, BeanDefinitionCustomizer...) */ @@ -521,7 +556,7 @@ public final void registerBean( * @param beanClass the class of the bean * @param supplier a callback for creating an instance of the bean * @param customizers one or more callbacks for customizing the factory's - * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * {@link BeanDefinition}, for example, setting a lazy-init or primary flag * @since 5.0 * @see #registerBean(String, Class, Supplier, BeanDefinitionCustomizer...) */ @@ -543,7 +578,7 @@ public final void registerBean( * @param supplier a callback for creating an instance of the bean (in case * of {@code null}, resolving a public constructor to be autowired instead) * @param customizers one or more callbacks for customizing the factory's - * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * {@link BeanDefinition}, for example, setting a lazy-init or primary flag * @since 5.0 */ public void registerBean(@Nullable String beanName, Class beanClass, @@ -561,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 @@ -578,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 ac1569e319da..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 @@ -66,7 +66,7 @@ * * *

    Alternatively, load a Groovy bean definition script like the following - * from an external resource (e.g. an "applicationContext.groovy" file): + * from an external resource (for example, an "applicationContext.groovy" file): * *

      * import org.hibernate.SessionFactory
    @@ -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 6c36abd36e0e..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,12 +165,12 @@ 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);
     	}
     
     	/**
    -	 * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance)
    +	 * Retrieve the given MessageSourceResolvable (for example, an ObjectError instance)
     	 * in the default Locale.
     	 * @param resolvable the MessageSourceResolvable
     	 * @return the message
    @@ -181,7 +181,7 @@ public String getMessage(MessageSourceResolvable resolvable) throws NoSuchMessag
     	}
     
     	/**
    -	 * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance)
    +	 * Retrieve the given MessageSourceResolvable (for example, an ObjectError instance)
     	 * in the given Locale.
     	 * @param resolvable the MessageSourceResolvable
     	 * @param locale the Locale in which to do lookup
    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 7735045a9808..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
    @@ -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.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.
    @@ -204,7 +204,7 @@ else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
     		invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);
     
     		// Clear cached merged bean definitions since the post-processors might have
    -		// modified the original metadata, e.g. replacing placeholders in values...
    +		// modified the original metadata, for example, replacing placeholders in values...
     		beanFactory.clearMetadataCache();
     	}
     
    @@ -226,7 +226,7 @@ public static void registerBeanPostProcessors(
     
     		String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);
     
    -		// Register BeanPostProcessorChecker that logs an info message when
    +		// Register BeanPostProcessorChecker that logs a warn message when
     		// a bean is created during BeanPostProcessor instantiation, i.e. when
     		// a bean is not eligible for getting processed by all BeanPostProcessors.
     		int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
    @@ -384,7 +384,7 @@ private static void registerBeanPostProcessors(
     
     
     	/**
    -	 * BeanPostProcessor that logs an info message when a bean is created during
    +	 * BeanPostProcessor that logs a warn message when a bean is created during
     	 * BeanPostProcessor instantiation, i.e. when a bean is not eligible for
     	 * getting processed by all BeanPostProcessors.
     	 */
    @@ -437,8 +437,9 @@ public Object postProcessAfterInitialization(Object bean, String beanName) {
     					logger.warn("Bean '" + beanName + "' of type [" + bean.getClass().getName() +
     							"] is not eligible for getting processed by all BeanPostProcessors " +
     							"(for example: not eligible for auto-proxying). Is this bean getting eagerly " +
    -							"injected into a currently created BeanPostProcessor " + bppsInCreation + "? " +
    -							"Check the corresponding BeanPostProcessor declaration and its dependencies.");
    +							"injected/applied to a currently created BeanPostProcessor " + bppsInCreation + "? " +
    +							"Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. " +
    +							"If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.");
     				}
     			}
     			return bean;
    @@ -492,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 1fd84402c121..36ee67f3a92b 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-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.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;
    @@ -33,7 +35,6 @@
     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;
     
    @@ -48,7 +49,7 @@
      * {@code PropertyPlaceholderConfigurer} to ensure backward compatibility. See the spring-context
      * XSD documentation for complete details.
      *
    - * 

    Any local properties (e.g. those added via {@link #setProperties}, {@link #setLocations} + *

    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 * 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 @@ -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; /** @@ -148,8 +146,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) this.propertySources.addLast( new PropertySource<>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { @Override - @Nullable - public String getProperty(String key) { + public @Nullable String getProperty(String key) { return propertyResolverToUse.getProperty(key); } } @@ -193,6 +190,7 @@ protected void processProperties(ConfigurableListableBeanFactory beanFactoryToPr propertyResolver.setPlaceholderPrefix(this.placeholderPrefix); propertyResolver.setPlaceholderSuffix(this.placeholderSuffix); propertyResolver.setValueSeparator(this.valueSeparator); + propertyResolver.setEscapeCharacter(this.escapeCharacter); StringValueResolver valueResolver = strVal -> { String resolved = (this.ignoreUnresolvablePlaceholders ? 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 18dffe1f0376..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 @@ -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. @@ -25,17 +25,18 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +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; @@ -63,7 +64,7 @@ * other than "-1" (caching forever) might not work reliably in this case. * *

    For a typical web application, message files could be placed in {@code WEB-INF}: - * e.g. a "WEB-INF/messages" basename would find a "WEB-INF/messages.properties", + * for example, a "WEB-INF/messages" basename would find a "WEB-INF/messages.properties", * "WEB-INF/messages_en.properties" etc arrangement as well as "WEB-INF/messages.xml", * "WEB-INF/messages_en.xml" etc. Note that message definitions in a previous * resource bundle will override ones in a later bundle, due to sequential lookup. @@ -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; @@ -137,7 +137,7 @@ public void setFileExtensions(List fileExtensions) { *

    Only applies to classic properties files, not to XML files. * @param fileEncodings a Properties with filenames as keys and charset * names as values. Filenames have to match the basename syntax, - * with optional locale-specific components: e.g. "WEB-INF/messages" + * with optional locale-specific components: for example, "WEB-INF/messages" * or "WEB-INF/messages_en". * @see #setBasenames * @see org.springframework.util.PropertiesPersister#load @@ -191,7 +191,7 @@ public void setResourceLoader(@Nullable ResourceLoader resourceLoader) { * returning the value found in the bundle as-is (without MessageFormat parsing). */ @Override - 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); @@ -219,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); @@ -251,36 +250,66 @@ protected MessageFormat resolveCode(String code, Locale locale) { *

    Only used when caching resource bundle contents forever, i.e. * with cacheSeconds < 0. Therefore, merged properties are always * cached forever. + * @see #collectPropertiesToMerge + * @see #mergeProperties */ protected PropertiesHolder getMergedProperties(Locale locale) { PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale); if (mergedHolder != null) { return mergedHolder; } + mergedHolder = mergeProperties(collectPropertiesToMerge(locale)); + PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder); + if (existing != null) { + mergedHolder = existing; + } + return mergedHolder; + } - Properties mergedProps = newProperties(); - long latestTimestamp = -1; + /** + * Determine the properties to merge based on the specified basenames. + * @param locale the locale + * @return the list of properties holders + * @since 6.1.4 + * @see #getBasenameSet() + * @see #calculateAllFilenames + * @see #mergeProperties + */ + protected List collectPropertiesToMerge(Locale locale) { String[] basenames = StringUtils.toStringArray(getBasenameSet()); + List holders = new ArrayList<>(basenames.length); for (int i = basenames.length - 1; i >= 0; i--) { List filenames = calculateAllFilenames(basenames[i], locale); for (int j = filenames.size() - 1; j >= 0; j--) { String filename = filenames.get(j); PropertiesHolder propHolder = getProperties(filename); if (propHolder.getProperties() != null) { - mergedProps.putAll(propHolder.getProperties()); - if (propHolder.getFileTimestamp() > latestTimestamp) { - latestTimestamp = propHolder.getFileTimestamp(); - } + holders.add(propHolder); } } } + return holders; + } - mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp); - PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder); - if (existing != null) { - mergedHolder = existing; + /** + * Merge the given properties holders into a single holder. + * @param holders the list of properties holders + * @return a single merged properties holder + * @since 6.1.4 + * @see #newProperties() + * @see #getMergedProperties + * @see #collectPropertiesToMerge + */ + protected PropertiesHolder mergeProperties(List holders) { + Properties mergedProps = newProperties(); + long latestTimestamp = -1; + for (PropertiesHolder holder : holders) { + mergedProps.putAll(holder.getProperties()); + if (holder.getFileTimestamp() > latestTimestamp) { + latestTimestamp = holder.getFileTimestamp(); + } } - return mergedHolder; + return new PropertiesHolder(mergedProps, latestTimestamp); } /** @@ -431,7 +460,7 @@ protected PropertiesHolder refreshProperties(String filename, @Nullable Properti long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis()); Resource resource = resolveResource(filename); - if (resource.exists()) { + if (resource != null) { long fileTimestamp = -1; if (getCacheMillis() >= 0) { // Last-modified timestamp of file will just be read if caching with timeout. @@ -508,18 +537,17 @@ protected PropertiesHolder refreshProperties(String filename, @Nullable Properti * {@link PropertiesPersister#load(Properties, InputStream) load} methods * for other types of resources. * @param filename the bundle filename (basename + Locale) - * @return the {@code Resource} to use + * @return the {@code Resource} to use, or {@code null} if none found * @since 6.1 */ - protected Resource resolveResource(String filename) { - Resource resource = null; + protected @Nullable Resource resolveResource(String filename) { for (String fileExtension : this.fileExtensions) { - resource = this.resourceLoader.getResource(filename + fileExtension); + Resource resource = this.resourceLoader.getResource(filename + fileExtension); if (resource.exists()) { return resource; } } - return Objects.requireNonNull(resource); + return null; } /** @@ -614,14 +642,13 @@ public String toString() { */ protected class PropertiesHolder { - @Nullable - private final Properties properties; + private final @Nullable Properties properties; private final long fileTimestamp; private volatile long refreshTimestamp = -2; - private final ReentrantLock refreshLock = new ReentrantLock(); + private final Lock refreshLock = new ReentrantLock(); /** Cache to hold already generated MessageFormats per message code. */ private final ConcurrentMap> cachedMessageFormats = @@ -637,8 +664,7 @@ public PropertiesHolder(Properties properties, long fileTimestamp) { this.fileTimestamp = fileTimestamp; } - @Nullable - public Properties getProperties() { + public @Nullable Properties getProperties() { return this.properties; } @@ -654,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 012237889a49..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,7 +142,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { * returning the value found in the bundle as-is (without MessageFormat parsing). */ @Override - 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); @@ -164,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); @@ -188,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. @@ -242,12 +237,12 @@ protected ResourceBundle doGetBundle(String basename, Locale locale) throws Miss return ResourceBundle.getBundle(basename, locale, classLoader, control); } catch (UnsupportedOperationException ex) { - // Probably in a Jigsaw environment on JDK 9+ + // Probably in a Java Module System environment on JDK 9+ this.control = null; String encoding = getDefaultEncoding(); if (encoding != null && logger.isInfoEnabled()) { logger.info("ResourceBundleMessageSource is configured to read resources with encoding '" + - encoding + "' but ResourceBundle.Control not supported in current system environment: " + + encoding + "' but ResourceBundle.Control is not supported in current system environment: " + ex.getMessage() + " - falling back to plain ResourceBundle.getBundle retrieval with the " + "platform default encoding. Consider setting the 'defaultEncoding' property to 'null' " + "for participating in the platform default and therefore avoiding this log message."); @@ -310,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); @@ -356,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); @@ -387,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 @@ -435,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/LocalStatelessSessionBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java index 035fad6581f5..14f6f88b10a1 100644 --- a/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -18,12 +18,13 @@ import org.w3c.dom.Element; +import org.springframework.beans.BeanUtils; import org.springframework.jndi.JndiObjectFactoryBean; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} * implementation for parsing '{@code local-slsb}' tags and - * creating plain {@link JndiObjectFactoryBean} definitions. + * creating plain {@link JndiObjectFactoryBean} definitions on 6.0. * * @author Rob Harrop * @author Juergen Hoeller @@ -36,4 +37,10 @@ protected Class getBeanClass(Element element) { return JndiObjectFactoryBean.class; } + @Override + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && + BeanUtils.getPropertyDescriptor(JndiObjectFactoryBean.class, extractPropertyName(attributeName)) != null); + } + } diff --git a/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java index a88c5f646189..9768e0dfc337 100644 --- a/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -18,12 +18,13 @@ import org.w3c.dom.Element; +import org.springframework.beans.BeanUtils; import org.springframework.jndi.JndiObjectFactoryBean; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} * implementation for parsing '{@code remote-slsb}' tags and - * creating plain {@link JndiObjectFactoryBean} definitions. + * creating plain {@link JndiObjectFactoryBean} definitions as of 6.0. * * @author Rob Harrop * @author Juergen Hoeller @@ -36,4 +37,10 @@ protected Class getBeanClass(Element element) { return JndiObjectFactoryBean.class; } + @Override + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && + BeanUtils.getPropertyDescriptor(JndiObjectFactoryBean.class, extractPropertyName(attributeName)) != null); + } + } 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/DateTimeFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java index 435ad2b7ec24..bb542896c2ee 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.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. @@ -25,15 +25,29 @@ /** * Declares that a field or method parameter should be formatted as a date or time. * - *

    Supports formatting by style pattern, ISO date time pattern, or custom format pattern string. + *

    Formatting applies to parsing a date/time object from a string as well as printing a + * date/time object to a string. + * + *

    Supports formatting by style pattern, ISO date/time pattern, or custom format pattern string. * Can be applied to {@link java.util.Date}, {@link java.util.Calendar}, {@link Long} (for * millisecond timestamps) as well as JSR-310 {@code java.time} value types. * *

    For style-based formatting, set the {@link #style} attribute to the desired style pattern code. * The first character of the code is the date style, and the second character is the time style. * Specify a character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for full. - * The date or time may be omitted by specifying the style character '-' — for example, - * 'M-' specifies a medium format for the date with no time. + * The date or time may be omitted by specifying the style character '-'. For example, + * 'M-' specifies a medium format for the date with no time. The supported style pattern codes + * correlate to the enum constants defined in {@link java.time.format.FormatStyle}. + * + *

    WARNING: Style-based formatting and parsing rely on locale-sensitive + * patterns which may change depending on the Java runtime. Specifically, applications that + * rely on date/time parsing and formatting may encounter incompatible changes in behavior + * when running on JDK 20 or higher. Using an ISO standardized format or a concrete pattern + * that you control allows for reliable system-independent and locale-independent parsing and + * formatting of date/time values. The use of {@linkplain #fallbackPatterns() fallback patterns} + * can also help to address compatibility issues. For further details, see the + * + * Date and Time Formatting with JDK 20 and higher page in the Spring Framework wiki. * *

    For ISO-based formatting, set the {@link #iso} attribute to the desired {@link ISO} format, * such as {@link ISO#DATE}. @@ -65,6 +79,8 @@ * @author Juergen Hoeller * @author Sam Brannen * @since 3.0 + * @see java.text.DateFormat + * @see java.text.SimpleDateFormat * @see java.time.format.DateTimeFormatter */ @Documented @@ -77,6 +93,8 @@ *

    Defaults to 'SS' for short date, short time. Set this attribute when you * wish to format your field or method parameter in accordance with a common * style other than the default style. + *

    See the {@linkplain DateTimeFormat class-level documentation} for further + * details. * @see #fallbackPatterns */ String style() default "SS"; @@ -93,13 +111,13 @@ /** * The custom pattern to use to format the field or method parameter. - *

    Defaults to empty String, indicating no custom pattern String has been + *

    Defaults to an empty String, indicating no custom pattern String has been * specified. Set this attribute when you wish to format your field or method * parameter in accordance with a custom date time pattern not represented by * a style or ISO format. - *

    Note: This pattern follows the original {@link java.text.SimpleDateFormat} style, - * as also supported by Joda-Time, with strict parsing semantics towards overflows - * (e.g. rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy' + *

    Note: This pattern follows the original {@link java.text.SimpleDateFormat} + * style, with strict parsing semantics towards overflows (for example, rejecting + * a {@code Feb 29} value for a non-leap-year). As a consequence, 'yy' * characters indicate a year in the traditional style, not a "year-of-era" as in the * {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu' * when going through a {@code DateTimeFormatter} with strict resolution mode). @@ -121,7 +139,6 @@ * or {@link #style} attribute is always used for printing. For details on * which time zone is used for fallback patterns, see the * {@linkplain DateTimeFormat class-level documentation}. - *

    Fallback patterns are not supported for Joda-Time value types. * @since 5.3.5 */ String[] fallbackPatterns() default {}; 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 new file mode 100644 index 000000000000..a9fb8adae564 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java @@ -0,0 +1,233 @@ +/* + * 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.format.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +/** + * Declares that a field or method parameter should be formatted as a + * {@link java.time.Duration}, according to the specified {@link #style Style} + * and {@link #defaultUnit Unit}. + * + * @author Simon Baslé + * @since 6.2 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +public @interface DurationFormat { + + /** + * The {@link Style} to use for parsing and printing a {@link Duration}. + *

    Defaults to the JDK style ({@link Style#ISO8601}). + */ + Style style() default Style.ISO8601; + + /** + * The {@link Unit} to fall back to in case the {@link #style Style} needs a unit + * for either parsing or printing, and none is explicitly provided in the input. + *

    Defaults to {@link Unit#MILLIS} if unspecified. + */ + Unit defaultUnit() default Unit.MILLIS; + + /** + * {@link Duration} format styles. + */ + enum Style { + + /** + * Simple formatting based on a short suffix, for example '1s'. + *

    Supported unit suffixes include: {@code ns, us, ms, s, m, h, d}. + * Those correspond to nanoseconds, microseconds, milliseconds, seconds, + * minutes, hours, and days, respectively. + *

    Note that when printing a {@link Duration}, this style can be + * lossy if the selected unit is bigger than the resolution of the + * duration. For example, {@code Duration.ofMillis(5).plusNanos(1234)} + * would get truncated to {@code "5ms"} when printing using + * {@code ChronoUnit.MILLIS}. + *

    Fractional durations are not supported. + */ + SIMPLE, + + /** + * ISO-8601 formatting. + *

    This is what the JDK uses in {@link Duration#parse(CharSequence)} + * and {@link Duration#toString()}. + */ + ISO8601, + + /** + * Like {@link #SIMPLE}, but allows multiple segments ordered from + * largest-to-smallest units of time, like {@code 1h12m27s}. + *

    A single minus sign ({@code -}) is allowed to indicate the whole + * duration is negative. Spaces are allowed between segments, and a + * negative duration with spaced segments can optionally be surrounded + * by parentheses after the minus sign, like so: {@code -(34m 57s)}. + */ + COMPOSITE + } + + /** + * {@link Duration} format unit, which mirrors a subset of {@link ChronoUnit} and + * allows conversion to and from a supported {@code ChronoUnit} as well as + * conversion from durations to longs. + * + *

    The enum includes its corresponding suffix in the {@link Style#SIMPLE SIMPLE} + * {@code Duration} format style. + */ + enum Unit { + + /** + * Nanoseconds ({@code "ns"}). + */ + NANOS(ChronoUnit.NANOS, "ns", Duration::toNanos), + + /** + * Microseconds ({@code "us"}). + */ + MICROS(ChronoUnit.MICROS, "us", duration -> duration.toNanos() / 1000L), + + /** + * Milliseconds ({@code "ms"}). + */ + MILLIS(ChronoUnit.MILLIS, "ms", Duration::toMillis), + + /** + * Seconds ({@code "s"}). + */ + SECONDS(ChronoUnit.SECONDS, "s", Duration::toSeconds), + + /** + * Minutes ({@code "m"}). + */ + MINUTES(ChronoUnit.MINUTES, "m", Duration::toMinutes), + + /** + * Hours ({@code "h"}). + */ + HOURS(ChronoUnit.HOURS, "h", Duration::toHours), + + /** + * Days ({@code "d"}). + */ + DAYS(ChronoUnit.DAYS, "d", Duration::toDays); + + private final ChronoUnit chronoUnit; + + private final String suffix; + + private final Function longValue; + + Unit(ChronoUnit chronoUnit, String suffix, Function toUnit) { + this.chronoUnit = chronoUnit; + this.suffix = suffix; + this.longValue = toUnit; + } + + /** + * Convert this {@code Unit} to its {@link ChronoUnit} equivalent. + */ + public ChronoUnit asChronoUnit() { + return this.chronoUnit; + } + + /** + * Convert this {@code Unit} to a simple {@code String} suffix, suitable + * for the {@link Style#SIMPLE SIMPLE} style. + */ + public String asSuffix() { + return this.suffix; + } + + /** + * Parse a {@code long} from the given {@link String} and interpret it to be a + * {@link Duration} in the current unit. + * @param value the {@code String} representation of the long + * @return the corresponding {@code Duration} + */ + public Duration parse(String value) { + return Duration.of(Long.parseLong(value), asChronoUnit()); + } + + /** + * Print the given {@link Duration} as a {@link String}, converting it to a long + * value using this unit's precision via {@link #longValue(Duration)} + * and appending this unit's simple {@link #asSuffix() suffix}. + * @param value the {@code Duration} to convert to a {@code String} + * @return the {@code String} representation of the {@code Duration} in the + * {@link Style#SIMPLE SIMPLE} style + */ + public String print(Duration value) { + return longValue(value) + asSuffix(); + } + + /** + * Convert the given {@link Duration} to a long value in the resolution + * of this unit. + *

    Note that this can be lossy if the current unit is bigger than the + * actual resolution of the duration. For example, + * {@code Duration.ofMillis(5).plusNanos(1234)} would get truncated to + * {@code 5} for unit {@code MILLIS}. + * @param value the {@code Duration} to convert to a long + * @return the long value for the {@code Duration} in this {@code Unit} + */ + public long longValue(Duration value) { + return this.longValue.apply(value); + } + + /** + * Get the {@link Unit} corresponding to the given {@link ChronoUnit}. + * @throws IllegalArgumentException if the given {@code ChronoUnit} is + * not supported + */ + public static Unit fromChronoUnit(@Nullable ChronoUnit chronoUnit) { + if (chronoUnit == null) { + return Unit.MILLIS; + } + for (Unit candidate : values()) { + if (candidate.chronoUnit == chronoUnit) { + return candidate; + } + } + throw new IllegalArgumentException("No matching Unit for ChronoUnit." + chronoUnit.name()); + } + + /** + * Get the {@link Unit} corresponding to the given {@link String} suffix. + * @throws IllegalArgumentException if the given suffix is not supported + */ + public static Unit fromSuffix(String suffix) { + for (Unit candidate : values()) { + if (candidate.suffix.equalsIgnoreCase(suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("'" + suffix + "' is not a valid simple duration Unit"); + } + + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java index c51f70b7ee48..ce0676bf96ec 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -74,7 +74,7 @@ enum Style { /** * The default format for the annotated type: typically 'number' but possibly - * 'currency' for a money type (e.g. {@code javax.money.MonetaryAmount)}. + * 'currency' for a money type (for example, {@code javax.money.MonetaryAmount}). * @since 4.2 */ DEFAULT, 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 4ba09ec5ed35..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 formatting rules. + * 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 06a95b7bee5a..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 @@ -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,22 +22,30 @@ import java.util.Collections; import java.util.Date; import java.util.EnumMap; +import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; +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; /** * A formatter for {@link java.util.Date} types. + * *

    Supports the configuration of an explicit date time pattern, timezone, * locale, and fallback date time patterns for lenient parsing. * + *

    Common ISO patterns for UTC instants are applied at millisecond precision. + * Note that {@link org.springframework.format.datetime.standard.InstantFormatter} + * is recommended for flexible UTC parsing into a {@link java.time.Instant} instead. + * * @author Keith Donald * @author Juergen Hoeller * @author Phillip Webb @@ -49,37 +57,39 @@ public class DateFormatter implements Formatter { private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); - // We use an EnumMap instead of Map.of(...) since the former provides better performance. private static final Map ISO_PATTERNS; + private static final Map ISO_FALLBACK_PATTERNS; + static { + // We use an EnumMap instead of Map.of(...) since the former provides better performance. Map formats = new EnumMap<>(ISO.class); formats.put(ISO.DATE, "yyyy-MM-dd"); formats.put(ISO.TIME, "HH:mm:ss.SSSXXX"); formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); ISO_PATTERNS = Collections.unmodifiableMap(formats); + + // Fallback format for the time part without milliseconds. + Map fallbackFormats = new EnumMap<>(ISO.class); + fallbackFormats.put(ISO.TIME, "HH:mm:ssXXX"); + fallbackFormats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ssXXX"); + ISO_FALLBACK_PATTERNS = Collections.unmodifiableMap(fallbackFormats); } - @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; @@ -166,7 +176,6 @@ public void setStyle(int style) { *

  • 'F' = Full
  • *
  • '-' = Omitted
  • * - * This method mimics the styles supported by Joda-Time. * @param stylePattern two characters from the set {"S", "M", "L", "F", "-"} * @since 3.2 */ @@ -202,8 +211,16 @@ public Date parse(String text, Locale locale) throws ParseException { return getDateFormat(locale).parse(text); } catch (ParseException ex) { + Set fallbackPatterns = new LinkedHashSet<>(); + String isoPattern = ISO_FALLBACK_PATTERNS.get(this.iso); + if (isoPattern != null) { + fallbackPatterns.add(isoPattern); + } if (!ObjectUtils.isEmpty(this.fallbackPatterns)) { - for (String pattern : this.fallbackPatterns) { + Collections.addAll(fallbackPatterns, this.fallbackPatterns); + } + if (!fallbackPatterns.isEmpty()) { + for (String pattern : fallbackPatterns) { try { DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale)); // Align timezone for parsing format with printing format if ISO is set. @@ -221,8 +238,8 @@ public Date parse(String text, Locale locale) throws ParseException { } if (this.source != null) { ParseException parseException = new ParseException( - String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), - ex.getErrorOffset()); + String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), + ex.getErrorOffset()); parseException.initCause(ex); throw parseException; } @@ -269,7 +286,7 @@ private DateFormat createDateFormat(Locale locale) { if (timeStyle != -1) { return DateFormat.getTimeInstance(timeStyle, locale); } - throw new IllegalStateException("Unsupported style pattern '" + this.stylePattern + "'"); + throw unsupportedStylePatternException(); } return DateFormat.getDateInstance(this.style, locale); @@ -277,15 +294,21 @@ private DateFormat createDateFormat(Locale locale) { private int getStylePatternForChar(int index) { if (this.stylePattern != null && this.stylePattern.length() > index) { - switch (this.stylePattern.charAt(index)) { - case 'S': return DateFormat.SHORT; - case 'M': return DateFormat.MEDIUM; - case 'L': return DateFormat.LONG; - case 'F': return DateFormat.FULL; - case '-': return -1; - } + char ch = this.stylePattern.charAt(index); + return switch (ch) { + case 'S' -> DateFormat.SHORT; + case 'M' -> DateFormat.MEDIUM; + case 'L' -> DateFormat.LONG; + case 'F' -> DateFormat.FULL; + case '-' -> -1; + default -> throw unsupportedStylePatternException(); + }; } - throw new IllegalStateException("Unsupported style pattern '" + this.stylePattern + "'"); + throw unsupportedStylePatternException(); + } + + private IllegalStateException unsupportedStylePatternException() { + return new IllegalStateException("Unsupported style pattern '" + this.stylePattern + "'"); } } 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 35f361fe6547..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 @@ -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. @@ -19,17 +19,18 @@ 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; /** * Configures basic date formatting for use with Spring, primarily for * {@link org.springframework.format.annotation.DateTimeFormat} declarations. - * Applies to fields of type {@link Date}, {@link Calendar} and {@code long}. + * Applies to fields of type {@link Date}, {@link Calendar}, and {@code long}. * *

    Designed for direct instantiation but also exposes the static * {@link #addDateConverters(ConverterRegistry)} utility method for @@ -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 3d0f395a0846..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 @@ -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,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; /** @@ -116,7 +112,7 @@ public void setDateTimeStyle(FormatStyle dateTimeStyle) { } /** - * Set the two characters to use to format date values, in Joda-Time style. + * Set the two characters to use to format date values. *

    The first character is used for the date style; the second is for * the time style. Supported characters are: *

      @@ -126,9 +122,9 @@ public void setDateTimeStyle(FormatStyle dateTimeStyle) { *
    • 'F' = Full
    • *
    • '-' = Omitted
    • *
    - *

    This method mimics the styles supported by Joda-Time. Note that - * JSR-310 natively favors {@link java.time.format.FormatStyle} as used for - * {@link #setDateStyle}, {@link #setTimeStyle} and {@link #setDateTimeStyle}. + *

    Note that JSR-310 natively favors {@link java.time.format.FormatStyle} + * as used for {@link #setDateStyle}, {@link #setTimeStyle}, and + * {@link #setDateTimeStyle}. * @param style two characters from the set {"S", "M", "L", "F", "-"} */ public void setStylePattern(String style) { @@ -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 1a72d4494938..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 @@ -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. @@ -51,6 +51,7 @@ * @see org.springframework.format.FormatterRegistrar#registerFormatters * @see org.springframework.format.datetime.DateFormatterRegistrar */ +@SuppressWarnings("NullAway") // Well-known map keys public class DateTimeFormatterRegistrar implements FormatterRegistrar { private enum Type {DATE, TIME, DATE_TIME} @@ -76,8 +77,8 @@ public DateTimeFormatterRegistrar() { /** * Set whether standard ISO formatting should be applied to all date/time types. - * Default is "false" (no). - *

    If set to "true", the "dateStyle", "timeStyle" and "dateTimeStyle" + *

    Default is "false" (no). + *

    If set to "true", the "dateStyle", "timeStyle", and "dateTimeStyle" * properties are effectively ignored. */ public void setUseIsoFormat(boolean useIsoFormat) { @@ -88,7 +89,7 @@ public void setUseIsoFormat(boolean useIsoFormat) { /** * Set the default format style of {@link java.time.LocalDate} objects. - * Default is {@link java.time.format.FormatStyle#SHORT}. + *

    Default is {@link java.time.format.FormatStyle#SHORT}. */ public void setDateStyle(FormatStyle dateStyle) { this.factories.get(Type.DATE).setDateStyle(dateStyle); @@ -96,7 +97,7 @@ public void setDateStyle(FormatStyle dateStyle) { /** * Set the default format style of {@link java.time.LocalTime} objects. - * Default is {@link java.time.format.FormatStyle#SHORT}. + *

    Default is {@link java.time.format.FormatStyle#SHORT}. */ public void setTimeStyle(FormatStyle timeStyle) { this.factories.get(Type.TIME).setTimeStyle(timeStyle); @@ -104,7 +105,7 @@ public void setTimeStyle(FormatStyle timeStyle) { /** * Set the default format style of {@link java.time.LocalDateTime} objects. - * Default is {@link java.time.format.FormatStyle#SHORT}. + *

    Default is {@link java.time.format.FormatStyle#SHORT}. */ public void setDateTimeStyle(FormatStyle dateTimeStyle) { this.factories.get(Type.DATE_TIME).setDateTimeStyle(dateTimeStyle); @@ -138,7 +139,7 @@ public void setTimeFormatter(DateTimeFormatter formatter) { /** * Set the formatter that will be used for objects representing date and time values. - *

    This formatter will be used for {@link LocalDateTime}, {@link ZonedDateTime} + *

    This formatter will be used for {@link LocalDateTime}, {@link ZonedDateTime}, * and {@link OffsetDateTime} types. When specified, the * {@link #setDateTimeStyle dateTimeStyle} and * {@link #setUseIsoFormat useIsoFormat} properties will be ignored. @@ -197,6 +198,7 @@ public void registerFormatters(FormatterRegistry registry) { registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter()); registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory()); + registry.addFormatterForFieldAnnotation(new DurationFormatAnnotationFormatterFactory()); } private DateTimeFormatter getFormatter(Type type) { diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java index be767f0e23c9..6317b8208a7a 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.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. @@ -29,10 +29,18 @@ */ abstract class DateTimeFormatterUtils { + /** + * Create a {@link DateTimeFormatter} for the supplied pattern, configured with + * {@linkplain ResolverStyle#STRICT strict} resolution. + *

    Note that the strict resolution does not affect the parsing. + * @param pattern the pattern to use + * @return a new {@code DateTimeFormatter} + * @see ResolverStyle#STRICT + */ static DateTimeFormatter createStrictDateTimeFormatter(String pattern) { - // Using strict parsing to align with Joda-Time and standard DateFormat behavior: - // otherwise, an overflow like e.g. Feb 29 for a non-leap-year wouldn't get rejected. - // However, with strict parsing, a year digit needs to be specified as 'u'... + // Using strict resolution to align with standard DateFormat behavior: + // otherwise, an overflow like, for example, Feb 29 for a non-leap-year wouldn't get rejected. + // However, with strict resolution, a year digit needs to be specified as 'u'... String patternToUse = StringUtils.replace(pattern, "yy", "uu"); return DateTimeFormatter.ofPattern(patternToUse).withResolverStyle(ResolverStyle.STRICT); } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatAnnotationFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatAnnotationFormatterFactory.java new file mode 100644 index 000000000000..52546a156b14 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatAnnotationFormatterFactory.java @@ -0,0 +1,57 @@ +/* + * 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.format.datetime.standard; + +import java.time.Duration; +import java.util.Set; + +import org.springframework.context.support.EmbeddedValueResolutionSupport; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.DurationFormat; + +/** + * Formats fields annotated with the {@link DurationFormat} annotation using the + * selected style for parsing and printing JSR-310 {@code Duration}. + * + * @author Simon Baslé + * @since 6.2 + * @see DurationFormat + * @see DurationFormatter + */ +public class DurationFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport + implements AnnotationFormatterFactory { + + // Create the set of field types that may be annotated with @DurationFormat. + private static final Set> FIELD_TYPES = Set.of(Duration.class); + + @Override + public final Set> getFieldTypes() { + return FIELD_TYPES; + } + + @Override + public Printer getPrinter(DurationFormat annotation, Class fieldType) { + return new DurationFormatter(annotation.style(), annotation.defaultUnit()); + } + + @Override + public Parser getParser(DurationFormat annotation, Class fieldType) { + return new DurationFormatter(annotation.style(), annotation.defaultUnit()); + } +} 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 b36169bdddb9..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,26 +20,77 @@ import java.time.Duration; import java.util.Locale; +import org.jspecify.annotations.Nullable; + import org.springframework.format.Formatter; +import org.springframework.format.annotation.DurationFormat; /** * {@link Formatter} implementation for a JSR-310 {@link Duration}, - * following JSR-310's parsing rules for a Duration. + * following JSR-310's parsing rules for a Duration by default and + * supporting additional {@code DurationFormat.Style} styles. * * @author Juergen Hoeller - * @since 4.2.4 - * @see Duration#parse + * @since 6.2 + * @see DurationFormatterUtils + * @see DurationFormat.Style */ -class DurationFormatter implements Formatter { +public class DurationFormatter implements Formatter { + + private final DurationFormat.Style style; + + private final DurationFormat.@Nullable Unit defaultUnit; + + /** + * Create a {@code DurationFormatter} following JSR-310's parsing rules for a Duration + * (the {@link DurationFormat.Style#ISO8601 ISO-8601} style). + */ + DurationFormatter() { + this(DurationFormat.Style.ISO8601); + } + + /** + * Create a {@code DurationFormatter} in a specific {@link DurationFormat.Style}. + *

    When a unit is needed but cannot be determined (for example, printing a Duration in the + * {@code SIMPLE} style), {@code DurationFormat.Unit#MILLIS} is used. + */ + public DurationFormatter(DurationFormat.Style style) { + this(style, null); + } + + /** + * Create a {@code DurationFormatter} in a specific {@link DurationFormat.Style} with an + * optional {@code DurationFormat.Unit}. + *

    If a {@code defaultUnit} is specified, it may be used in parsing cases when no + * unit is present in the string (provided the style allows for such a case). It will + * also be used as the representation's resolution when printing in the + * {@link DurationFormat.Style#SIMPLE} style. Otherwise, the style defines its default + * unit. + * + * @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, DurationFormat.@Nullable Unit defaultUnit) { + this.style = style; + this.defaultUnit = defaultUnit; + } @Override public Duration parse(String text, Locale locale) throws ParseException { - return Duration.parse(text); + if (this.defaultUnit == null) { + //delegate to the style + return DurationFormatterUtils.parse(text, this.style); + } + return DurationFormatterUtils.parse(text, this.style, this.defaultUnit); } @Override public String print(Duration object, Locale locale) { - return object.toString(); + if (this.defaultUnit == null) { + //delegate the ultimate of the default unit to the style + return DurationFormatterUtils.print(object, this.style); + } + return DurationFormatterUtils.print(object, this.style, this.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 new file mode 100644 index 000000000000..8e7c502ba470 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatterUtils.java @@ -0,0 +1,247 @@ +/* + * 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.format.datetime.standard; + +import java.time.Duration; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jspecify.annotations.Nullable; + +import org.springframework.format.annotation.DurationFormat; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Support {@code Duration} parsing and printing in several styles, as listed in + * {@link DurationFormat.Style}. + *

    Some styles may not enforce any unit to be present, defaulting to {@code DurationFormat.Unit#MILLIS} + * in that case. Methods in this class offer overloads that take a {@link DurationFormat.Unit} to + * be used as a fall-back instead of the ultimate MILLIS default. + * + * @author Phillip Webb + * @author Valentine Wu + * @author Simon Baslé + * @since 6.2 + */ +public abstract class DurationFormatterUtils { + + private DurationFormatterUtils() { + // singleton + } + + /** + * Parse the given value to a duration. + * @param value the value to parse + * @param style the style in which to parse + * @return a duration + */ + public static Duration parse(String value, DurationFormat.Style style) { + return parse(value, style, null); + } + + /** + * Parse the given value to a duration. + * @param value the value to parse + * @param style the style in which to parse + * @param unit the duration unit to use if the value doesn't specify one ({@code null} + * will default to ms) + * @return a duration + */ + 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); + case SIMPLE -> parseSimple(value, unit); + case COMPOSITE -> parseComposite(value); + }; + } + + /** + * Print the specified duration in the specified style. + * @param value the value to print + * @param style the style to print in + * @return the printed result + */ + public static String print(Duration value, DurationFormat.Style style) { + return print(value, style, null); + } + + /** + * Print the specified duration in the specified style using the given unit. + * @param value the value to print + * @param style the style to print in + * @param unit the unit to use for printing, if relevant ({@code null} will default + * to ms) + * @return the printed result + */ + 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); + case COMPOSITE -> printComposite(value); + }; + } + + /** + * Detect the style then parse the value to return a duration. + * @param value the value to parse + * @return the parsed duration + * @throws IllegalArgumentException if the value is not a known style or cannot be + * parsed + */ + public static Duration detectAndParse(String value) { + return detectAndParse(value, null); + } + + /** + * Detect the style then parse the value to return a duration. + * @param value the value to parse + * @param unit the duration unit to use if the value doesn't specify one ({@code null} + * will default to ms) + * @return the parsed duration + * @throws IllegalArgumentException if the value is not a known style or cannot be + * parsed + */ + public static Duration detectAndParse(String value, DurationFormat.@Nullable Unit unit) { + return parse(value, detect(value), unit); + } + + /** + * Detect the style from the given source value. + * @param value the source value + * @return the duration style + * @throws IllegalArgumentException if the value is not a known style + */ + public static DurationFormat.Style detect(String value) { + Assert.notNull(value, "Value must not be null"); + // warning: the order of parsing starts to matter if multiple patterns accept a plain integer (no unit suffix) + if (ISO_8601_PATTERN.matcher(value).matches()) { + return DurationFormat.Style.ISO8601; + } + if (SIMPLE_PATTERN.matcher(value).matches()) { + return DurationFormat.Style.SIMPLE; + } + if (COMPOSITE_PATTERN.matcher(value).matches()) { + return DurationFormat.Style.COMPOSITE; + } + throw new IllegalArgumentException("'" + value + "' is not a valid duration, cannot detect any known style"); + } + + 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 Duration parseIso8601(String value) { + try { + return Duration.parse(value); + } + catch (Exception ex) { + throw new IllegalArgumentException("'" + value + "' is not a valid ISO-8601 duration", ex); + } + } + + 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"); + String suffix = matcher.group(2); + DurationFormat.Unit parsingUnit = (fallbackUnit == null ? DurationFormat.Unit.MILLIS : fallbackUnit); + if (StringUtils.hasLength(suffix)) { + parsingUnit = DurationFormat.Unit.fromSuffix(suffix); + } + return parsingUnit.parse(matcher.group(1)); + } + catch (Exception ex) { + throw new IllegalArgumentException("'" + text + "' is not a valid simple duration", ex); + } + } + + private static String printSimple(Duration duration, DurationFormat.@Nullable Unit unit) { + unit = (unit == null ? DurationFormat.Unit.MILLIS : unit); + return unit.print(duration); + } + + private static Duration parseComposite(String text) { + try { + Matcher matcher = COMPOSITE_PATTERN.matcher(text); + Assert.state(matcher.matches() && matcher.groupCount() > 1, "Does not match composite duration pattern"); + String sign = matcher.group(1); + boolean negative = sign != null && sign.equals("-"); + + Duration result = Duration.ZERO; + DurationFormat.Unit[] units = DurationFormat.Unit.values(); + for (int i = 2; i < matcher.groupCount() + 1; i++) { + String segment = matcher.group(i); + if (StringUtils.hasText(segment)) { + DurationFormat.Unit unit = units[units.length - i + 1]; + result = result.plus(unit.parse(segment.replace(unit.asSuffix(), ""))); + } + } + return negative ? result.negated() : result; + } + catch (Exception ex) { + throw new IllegalArgumentException("'" + text + "' is not a valid composite duration", ex); + } + } + + private static String printComposite(Duration duration) { + if (duration.isZero()) { + return DurationFormat.Unit.SECONDS.print(duration); + } + StringBuilder result = new StringBuilder(); + if (duration.isNegative()) { + result.append('-'); + duration = duration.negated(); + } + long days = duration.toDaysPart(); + if (days != 0) { + result.append(days).append(DurationFormat.Unit.DAYS.asSuffix()); + } + int hours = duration.toHoursPart(); + if (hours != 0) { + result.append(hours).append(DurationFormat.Unit.HOURS.asSuffix()); + } + int minutes = duration.toMinutesPart(); + if (minutes != 0) { + result.append(minutes).append(DurationFormat.Unit.MINUTES.asSuffix()); + } + int seconds = duration.toSecondsPart(); + if (seconds != 0) { + result.append(seconds).append(DurationFormat.Unit.SECONDS.asSuffix()); + } + int millis = duration.toMillisPart(); + if (millis != 0) { + result.append(millis).append(DurationFormat.Unit.MILLIS.asSuffix()); + } + //special handling of nanos: remove the millis part and then divide into microseconds and nanoseconds + long nanos = duration.toNanosPart() - Duration.ofMillis(millis).toNanos(); + if (nanos != 0) { + long micros = nanos / 1000; + long remainder = nanos - (micros * 1000); + if (micros > 0) { + result.append(micros).append(DurationFormat.Unit.MICROS.asSuffix()); + } + if (remainder > 0) { + result.append(remainder).append(DurationFormat.Unit.NANOS.asSuffix()); + } + } + return result.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index 5f02c4a78356..e40cb3174a76 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.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. @@ -45,12 +45,12 @@ public Instant parse(String text, Locale locale) throws ParseException { return Instant.ofEpochMilli(Long.parseLong(text)); } catch (NumberFormatException ex) { - if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) { + if (!text.isEmpty() && Character.isAlphabetic(text.charAt(0))) { // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)); } else { - // assuming UTC instant a la "2007-12-03T10:15:30.00Z" + // assuming UTC instant a la "2007-12-03T10:15:30.000Z" return Instant.parse(text); } } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthFormatter.java index abae18df61cb..a6c56fbe9354 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthFormatter.java @@ -34,7 +34,7 @@ class MonthFormatter implements Formatter { @Override public Month parse(String text, Locale locale) throws ParseException { - return Month.valueOf(text.toUpperCase()); + return Month.valueOf(text.toUpperCase(Locale.ROOT)); } @Override 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 aa3ecdbdbd68..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 @@ -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.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; @@ -97,7 +98,7 @@ public DefaultFormattingConversionService( /** * Add formatters appropriate for most environments: including number formatters, - * JSR-354 Money & Currency formatters, JSR-310 Date-Time and/or Joda-Time formatters, + * JSR-354 Money & Currency formatters, and JSR-310 Date-Time formatters, * depending on the presence of the corresponding API on the classpath. * @param formatterRegistry the service to register default formatters with */ 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 4ac53b009112..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 @@ -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,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,28 +30,22 @@ 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; /** - * A factory providing convenient access to a {@code FormattingConversionService} - * configured with converters and formatters for common types such as numbers and - * datetimes. + * A factory providing convenient access to a {@link FormattingConversionService} + * configured with converters and formatters for common types such as numbers, dates, + * and times. * *

    Additional converters and formatters can be registered declaratively through * {@link #setConverters(Set)} and {@link #setFormatters(Set)}. Another option * is to register converters and formatters in code by implementing the - * {@link FormatterRegistrar} interface. You can then configure provide the set - * of registrars to use through {@link #setFormatterRegistrars(Set)}. - * - *

    A good example for registering converters and formatters in code is - * {@code JodaTimeFormatterRegistrar}, which registers a number of - * date-related formatters and converters. For a more detailed list of cases - * see {@link #setFormatterRegistrars(Set)} + * {@link FormatterRegistrar} interface. You can then provide the set of registrars + * to use through {@link #setFormatterRegistrars(Set)}. * *

    Like all {@code FactoryBean} implementations, this class is suitable for * use when configuring a Spring application context using Spring {@code } - * XML. When configuring the container with + * XML configuration files. When configuring the container with * {@link org.springframework.context.annotation.Configuration @Configuration} * classes, simply instantiate, configure and return the appropriate * {@code FormattingConversionService} object from a @@ -64,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; /** @@ -167,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 a6e87cec0ca8..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,6 +16,8 @@ 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; @@ -29,7 +31,7 @@ class FormattingConversionServiceRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.reflection().registerType(TypeReference.of("javax.money.MonetaryAmount")); } } 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 47bedbc67df1..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; @@ -35,7 +36,7 @@ * follows where spring-instrument-{version}.jar is a JAR file * containing the {@link InstrumentationSavingAgent} class shipped with Spring * and where {version} is the release version of the Spring - * Framework (e.g., {@code 5.1.5.RELEASE}). + * Framework (for example, {@code 5.1.5.RELEASE}). * *

    -javaagent:path/to/spring-instrument-{version}.jar * @@ -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 6830b8983072..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,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; @@ -53,9 +53,6 @@ * web application). There is no direct API dependency between this LoadTimeWeaver * adapter and the underlying ClassLoader, just a 'loose' method contract. * - *

    This is the LoadTimeWeaver to use e.g. with the Resin application server - * version 3.1+. - * * @author Costin Leau * @author Juergen Hoeller * @since 2.0 @@ -76,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 03db47b6b473..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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,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<>(); @@ -85,7 +85,7 @@ public byte[] transformIfNecessary(String className, byte[] bytes) { * @param className the full qualified name of the class in dot format (i.e. some.package.SomeClass) * @param internalName class name internal name in / format (i.e. some/package/SomeClass) * @param bytes class byte definition - * @param pd protection domain to be used (can be null) + * @param pd protection domain to be used (can be {@code null}) * @return (possibly transformed) class byte definition */ public byte[] transformIfNecessary(String className, String internalName, byte[] bytes, @Nullable ProtectionDomain pd) { 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 f2b45c61fe8b..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 @@ -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. @@ -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; @@ -30,7 +31,7 @@ * {@link LoadTimeWeaver} implementation for GlassFish's * {@code org.glassfish.api.deployment.InstrumentableClassLoader InstrumentableClassLoader}. * - *

    As of Spring Framework 5.0, this weaver supports GlassFish 4+. + *

    This weaver supports GlassFish 4+. * * @author Costin Leau * @author Juergen Hoeller 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 cef5e337c5b7..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,22 +17,25 @@ package org.springframework.instrument.classloading.jboss; import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Constructor; 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; +import org.springframework.util.function.ThrowingFunction; /** * {@link LoadTimeWeaver} implementation for JBoss's instrumentable ClassLoader. * Thanks to Ales Justin and Marius Bogoevici for the initial prototype. * - *

    As of Spring Framework 5.0, this weaver supports WildFly 8+. - * As of Spring Framework 5.1.5, it also supports WildFly 13+. + *

    This weaver supports WildFly 13-23 (DelegatingClassFileTransformer) as well as + * WildFly 24+ (DelegatingClassTransformer), as of Spring Framework 6.1.15. * * @author Costin Leau * @author Juergen Hoeller @@ -40,9 +43,15 @@ */ public class JBossLoadTimeWeaver implements LoadTimeWeaver { - private static final String DELEGATING_TRANSFORMER_CLASS_NAME = + private static final String LEGACY_DELEGATING_TRANSFORMER_CLASS_NAME = "org.jboss.as.server.deployment.module.DelegatingClassFileTransformer"; + private static final String DELEGATING_TRANSFORMER_CLASS_NAME = + "org.jboss.as.server.deployment.module.DelegatingClassTransformer"; + + private static final String CLASS_TRANSFORMER_CLASS_NAME = + "org.jboss.modules.ClassTransformer"; + private static final String WRAPPER_TRANSFORMER_CLASS_NAME = "org.jboss.modules.JLIClassTransformer"; @@ -53,6 +62,8 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver { private final Method addTransformer; + private final ThrowingFunction adaptTransformer; + /** * Create a new instance of the {@link JBossLoadTimeWeaver} class using @@ -91,18 +102,29 @@ public JBossLoadTimeWeaver(@Nullable ClassLoader classLoader) { wrappedTransformer.setAccessible(true); suggestedTransformer = wrappedTransformer.get(suggestedTransformer); } - if (!suggestedTransformer.getClass().getName().equals(DELEGATING_TRANSFORMER_CLASS_NAME)) { + + Class transformerType = ClassFileTransformer.class; + if (suggestedTransformer.getClass().getName().equals(LEGACY_DELEGATING_TRANSFORMER_CLASS_NAME)) { + this.adaptTransformer = (t -> t); + } + else if (suggestedTransformer.getClass().getName().equals(DELEGATING_TRANSFORMER_CLASS_NAME)) { + transformerType = classLoader.loadClass(CLASS_TRANSFORMER_CLASS_NAME); + Constructor adaptedTransformer = classLoader.loadClass(WRAPPER_TRANSFORMER_CLASS_NAME) + .getConstructor(ClassFileTransformer.class); + this.adaptTransformer = adaptedTransformer::newInstance; + } + else { throw new IllegalStateException( - "Transformer not of the expected type DelegatingClassFileTransformer: " + + "Transformer not of expected type DelegatingClass(File)Transformer: " + suggestedTransformer.getClass().getName()); } this.delegatingTransformer = suggestedTransformer; Method addTransformer = ReflectionUtils.findMethod(this.delegatingTransformer.getClass(), - "addTransformer", ClassFileTransformer.class); + "addTransformer", transformerType); if (addTransformer == null) { throw new IllegalArgumentException( - "Could not find 'addTransformer' method on JBoss DelegatingClassFileTransformer: " + + "Could not find 'addTransformer' method on JBoss DelegatingClass(File)Transformer: " + this.delegatingTransformer.getClass().getName()); } addTransformer.setAccessible(true); @@ -117,7 +139,7 @@ public JBossLoadTimeWeaver(@Nullable ClassLoader classLoader) { @Override public void addTransformer(ClassFileTransformer transformer) { try { - this.addTransformer.invoke(this.delegatingTransformer, transformer); + this.addTransformer.invoke(this.delegatingTransformer, this.adaptTransformer.apply(transformer)); } catch (Throwable ex) { throw new IllegalStateException("Could not add transformer on JBoss ClassLoader: " + this.classLoader, ex); 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 ea16a2fed873..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,6 +18,8 @@ import javax.management.JMRuntimeException; +import org.jspecify.annotations.Nullable; + /** * Thrown when trying to invoke an operation on a proxy that is not exposed * by the proxied MBean resource's management interface. @@ -35,7 +37,7 @@ public class InvalidInvocationException extends JMRuntimeException { * error message. * @param msg the detail message */ - public InvalidInvocationException(String msg) { + public InvalidInvocationException(@Nullable String msg) { super(msg); } 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 1f41aaf60d99..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-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. @@ -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; } @@ -348,7 +337,7 @@ protected boolean isPrepared() { /** - * Route the invocation to the configured managed resource.. + * Route the invocation to the configured managed resource. * @param invocation the {@code MethodInvocation} to re-route * @return the value returned as a result of the re-routed invocation * @throws Throwable an invocation error propagated to the user @@ -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; @@ -439,7 +425,7 @@ protected Object doInvoke(MethodInvocation invocation) throws Throwable { throw ex.getTargetError(); } catch (RuntimeOperationsException ex) { - // This one is only thrown by the JMX 1.2 RI, not by the JDK 1.5 JMX code. + // This one is only thrown by the JMX 1.2 RI, not by the JDK JMX code. RuntimeException rex = ex.getTargetException(); if (rex instanceof RuntimeMBeanException runtimeMBeanException) { throw runtimeMBeanException.getTargetException(); @@ -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 f55de8e37b88..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 @@ -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,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; /** @@ -83,29 +81,31 @@ public void setBeanClassLoader(ClassLoader classLoader) { public void afterPropertiesSet() throws MBeanServerNotFoundException, MBeanInfoRetrievalException { super.afterPropertiesSet(); + Class interfaceToUse; if (this.proxyInterface == null) { - this.proxyInterface = getManagementInterface(); - if (this.proxyInterface == null) { + interfaceToUse = getManagementInterface(); + if (interfaceToUse == null) { throw new IllegalArgumentException("Property 'proxyInterface' or 'managementInterface' is required"); } + this.proxyInterface = interfaceToUse; } else { + interfaceToUse = this.proxyInterface; if (getManagementInterface() == null) { - setManagementInterface(this.proxyInterface); + setManagementInterface(interfaceToUse); } } - this.mbeanProxy = new ProxyFactory(this.proxyInterface, this).getProxy(this.beanClassLoader); + this.mbeanProxy = new ProxyFactory(interfaceToUse, this).getProxy(this.beanClassLoader); } @Override - @Nullable - public Object getObject() { + public @Nullable Object getObject() { return this.mbeanProxy; } @Override - 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 dd9484e9c1a5..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-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,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -39,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; @@ -63,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; @@ -75,7 +75,7 @@ * JMX-specific information in the bean classes. * *

    If a bean implements one of the JMX management interfaces, MBeanExporter can - * simply register the MBean with the server through its autodetection process. + * simply register the MBean with the server through its auto-detection process. * *

    If a bean does not implement one of the JMX management interfaces, MBeanExporter * will create the management information using the supplied {@link MBeanInfoAssembler}. @@ -104,21 +104,21 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo BeanClassLoaderAware, BeanFactoryAware, InitializingBean, SmartInitializingSingleton, DisposableBean { /** - * Autodetection mode indicating that no autodetection should be used. + * Auto-detection mode indicating that no auto-detection should be used. * @deprecated as of 6.1, in favor of the {@link #setAutodetect "autodetect" flag} */ @Deprecated(since = "6.1") public static final int AUTODETECT_NONE = 0; /** - * Autodetection mode indicating that only valid MBeans should be autodetected. + * Auto-detection mode indicating that only valid MBeans should be autodetected. * @deprecated as of 6.1, in favor of the {@link #setAutodetect "autodetect" flag} */ @Deprecated(since = "6.1") public static final int AUTODETECT_MBEAN = 1; /** - * Autodetection mode indicating that only the {@link MBeanInfoAssembler} should be able + * Auto-detection mode indicating that only the {@link MBeanInfoAssembler} should be able * to autodetect beans. * @deprecated as of 6.1, in favor of the {@link #setAutodetect "autodetect" flag} */ @@ -126,7 +126,7 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo public static final int AUTODETECT_ASSEMBLER = 2; /** - * Autodetection mode indicating that all autodetection mechanisms should be used. + * Auto-detection mode indicating that all auto-detection mechanisms should be used. * @deprecated as of 6.1, in favor of the {@link #setAutodetect "autodetect" flag} */ @Deprecated(since = "6.1") @@ -155,14 +155,12 @@ 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 autodetecting MBeans. */ + /** Whether to eagerly initialize candidate beans when auto-detecting MBeans. */ private boolean allowEagerInit = false; /** Stores the MBeanInfoAssembler to use for this exporter. */ @@ -177,27 +175,23 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo /** Indicates whether Spring should expose the managed resource ClassLoader in the MBean. */ private boolean exposeManagedResourceClassLoader = true; - /** A set of bean names that should be excluded from autodetection. */ + /** A set of bean names that should be excluded from auto-detection. */ 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 autodetection process. */ - @Nullable - private ListableBeanFactory beanFactory; + /** Stores the BeanFactory for use in auto-detection process. */ + private @Nullable ListableBeanFactory beanFactory; /** @@ -226,7 +220,7 @@ public void setBeans(Map beans) { * runs in. Will also ask an {@code AutodetectCapableMBeanInfoAssembler} * if available. *

    This feature is turned off by default. Explicitly specify - * {@code true} here to enable autodetection. + * {@code true} here to enable auto-detection. * @see #setAssembler * @see AutodetectCapableMBeanInfoAssembler * @see #isMBean @@ -236,7 +230,7 @@ public void setAutodetect(boolean autodetect) { } /** - * Set the autodetection mode to use by name. + * Set the auto-detection mode to use by name. * @throws IllegalArgumentException if the supplied value is not resolvable * to one of the {@code AUTODETECT_} constants or is {@code null} * @see #setAutodetectMode(int) @@ -255,7 +249,7 @@ public void setAutodetectModeName(String constantName) { } /** - * Set the autodetection mode to use. + * Set the auto-detection mode to use. * @throws IllegalArgumentException if the supplied value is not * one of the {@code AUTODETECT_} constants * @see #setAutodetectModeName(String) @@ -274,7 +268,7 @@ public void setAutodetectMode(int autodetectMode) { /** * Specify whether to allow eager initialization of candidate beans - * when autodetecting MBeans in the Spring application context. + * when auto-detecting MBeans in the Spring application context. *

    Default is "false", respecting lazy-init flags on bean definitions. * Switch this to "true" in order to search lazy-init beans as well, * including FactoryBean-produced objects that haven't been initialized yet. @@ -288,7 +282,7 @@ public void setAllowEagerInit(boolean allowEagerInit) { * for this exporter. Default is a {@code SimpleReflectiveMBeanInfoAssembler}. *

    The passed-in assembler can optionally implement the * {@code AutodetectCapableMBeanInfoAssembler} interface, which enables it - * to participate in the exporter's MBean autodetection process. + * to participate in the exporter's MBean auto-detection process. * @see org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler * @see org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler @@ -334,7 +328,7 @@ public void setExposeManagedResourceClassLoader(boolean exposeManagedResourceCla } /** - * Set the list of names for beans that should be excluded from autodetection. + * Set the list of names for beans that should be excluded from auto-detection. */ public void setExcludedBeans(String... excludedBeans) { this.excludedBeans.clear(); @@ -342,7 +336,7 @@ public void setExcludedBeans(String... excludedBeans) { } /** - * Add the name of bean that should be excluded from autodetection. + * Add the name of bean that should be excluded from auto-detection. */ public void addExcludedBean(String excludedBean) { Assert.notNull(excludedBean, "ExcludedBean must not be null"); @@ -411,7 +405,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { /** * This callback is only required for resolution of bean names in the * {@link #setBeans(java.util.Map) "beans"} {@link Map} and for - * autodetection of MBeans (in the latter case, a + * auto-detection of MBeans (in the latter case, a * {@code ListableBeanFactory} is required). * @see #setBeans * @see #setAutodetect @@ -422,7 +416,7 @@ public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = lbf; } else { - logger.debug("MBeanExporter not running in a ListableBeanFactory: autodetection of MBeans not available."); + logger.debug("MBeanExporter not running in a ListableBeanFactory: auto-detection of MBeans not available."); } } @@ -537,7 +531,7 @@ public void unregisterManagedResource(ObjectName objectName) { * implementation of the {@code ObjectNamingStrategy} interface being used. */ protected void registerBeans() { - // The beans property may be null, for example if we are relying solely on autodetection. + // The beans property may be null, for example if we are relying solely on auto-detection. if (this.beans == null) { this.beans = new HashMap<>(); // Use AUTODETECT_ALL as default in no beans specified explicitly. @@ -546,7 +540,7 @@ protected void registerBeans() { } } - // Perform autodetection, if desired. + // Perform auto-detection, if desired. int mode = (this.autodetectMode != null ? this.autodetectMode : AUTODETECT_NONE); if (mode != AUTODETECT_NONE) { if (this.beanFactory == null) { @@ -554,7 +548,7 @@ protected void registerBeans() { } if (mode == AUTODETECT_MBEAN || mode == AUTODETECT_ALL) { // Autodetect any beans that are already MBeans. - logger.debug("Autodetecting user-defined JMX MBeans"); + logger.debug("Auto-detecting user-defined JMX MBeans"); autodetect(this.beans, (beanClass, beanName) -> isMBean(beanClass)); } // Allow the assembler a chance to vote for bean inclusion. @@ -795,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); @@ -871,11 +864,11 @@ private ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws J //--------------------------------------------------------------------- - // Autodetection process + // auto-detection process //--------------------------------------------------------------------- /** - * Performs the actual autodetection process, delegating to an + * Performs the actual auto-detection process, delegating to an * {@code AutodetectCallback} instance to vote on the inclusion of a * given bean. * @param callback the {@code AutodetectCallback} to use when deciding @@ -883,7 +876,7 @@ private ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws J */ private void autodetect(Map beans, AutodetectCallback callback) { Assert.state(this.beanFactory != null, "No BeanFactory set"); - Set beanNames = new LinkedHashSet<>(this.beanFactory.getBeanDefinitionCount()); + Set beanNames = CollectionUtils.newLinkedHashSet(this.beanFactory.getBeanDefinitionCount()); Collections.addAll(beanNames, this.beanFactory.getBeanDefinitionNames()); if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { Collections.addAll(beanNames, cbf.getSingletonNames()); @@ -933,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 '&' } /** @@ -1074,13 +1067,13 @@ private void notifyListenersOfUnregistration(ObjectName objectName) { //--------------------------------------------------------------------- /** - * Internal callback interface for the autodetection process. + * Internal callback interface for the auto-detection process. */ @FunctionalInterface private interface AutodetectCallback { /** - * Called during the autodetection process to decide whether + * Called during the auto-detection process to decide whether * a bean should be included. * @param beanClass the class of the bean * @param beanName the name of the bean @@ -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; @@ -1112,7 +1103,6 @@ public void setObjectName(ObjectName objectName) { } @Override - @Nullable public Object getTarget() { try { return super.getTarget(); 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 975b40be7c59..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,9 @@ 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; /** @@ -60,8 +61,7 @@ */ public class AnnotationJmxAttributeSource implements JmxAttributeSource, BeanFactoryAware { - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; @Override @@ -73,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()) { @@ -86,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) -> { @@ -103,29 +105,32 @@ 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"); PropertyAccessorFactory.forBeanPropertyAccess(bean).setPropertyValues(pvs); String defaultValue = (String) map.get("defaultValue"); - if (defaultValue.length() > 0) { + if (StringUtils.hasLength(defaultValue)) { bean.setDefaultValue(defaultValue); } return bean; } @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(); @@ -133,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(); @@ -142,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) @@ -175,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); @@ -186,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 ce7da5dc2328..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; } @@ -266,7 +264,7 @@ protected boolean isExposeClassDescriptor() { /** * Set the ParameterNameDiscoverer to use for resolving method parameter - * names if needed (e.g. for parameter names of MBean operation methods). + * names if needed (for example, for parameter names of MBean operation methods). *

    Default is a {@link DefaultParameterNameDiscoverer}. */ public void setParameterNameDiscoverer(@Nullable ParameterNameDiscoverer parameterNameDiscoverer) { @@ -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/AutodetectCapableMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java index a033c01833fd..38519552fdb9 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -17,7 +17,7 @@ package org.springframework.jmx.export.assembler; /** - * Extends the {@code MBeanInfoAssembler} to add autodetection logic. + * Extends the {@code MBeanInfoAssembler} to add auto-detection logic. * Implementations of this interface are given the opportunity by the * {@code MBeanExporter} to include additional beans in the registration process. * 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 a174e5870bde..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -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; /** @@ -118,7 +119,7 @@ protected void checkManagedBean(Object managedBean) throws IllegalArgumentExcept } /** - * Used for autodetection of beans. Checks to see if the bean's class has a + * Used for auto-detection of beans. Checks to see if the bean's class has a * {@code ManagedResource} attribute. If so, it will add it to the list of included beans. * @param beanClass the class of the bean * @param beanName the name of the bean in the bean factory @@ -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; @@ -417,7 +418,7 @@ protected void populateOperationDescriptor(Descriptor desc, Method method, Strin * @param setter the int associated with the setter for this attribute */ private int resolveIntDescriptor(int getter, int setter) { - return (getter >= setter ? getter : setter); + return Math.max(getter, setter); } /** @@ -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 70243a86c15a..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,27 +62,24 @@ 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; /** - * Set local properties, containing object name mappings, e.g. via + * Set local properties, containing object name mappings, for example, via * the "props" tag in XML bean definitions. These can be considered * defaults, to be overridden by properties loaded from files. */ 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 c0a2c4d875e6..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * 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. @@ -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; @@ -50,14 +51,15 @@ */ public class MetadataNamingStrategy implements ObjectNamingStrategy, InitializingBean { + private static final char[] QUOTABLE_CHARS = new char[] {',', '=', ':', '"'}; + + /** * 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; /** @@ -132,10 +134,23 @@ public ObjectName getObjectName(Object managedBean, @Nullable String beanKey) th } Hashtable properties = new Hashtable<>(); properties.put("type", ClassUtils.getShortName(managedClass)); - properties.put("name", beanKey); + properties.put("name", quoteIfNecessary(beanKey)); return ObjectNameManager.getInstance(domain, properties); } } } + private static String quoteIfNecessary(String value) { + return shouldQuote(value) ? ObjectName.quote(value) : value; + } + + private static boolean shouldQuote(String value) { + for (char quotableChar : QUOTABLE_CHARS) { + if (value.indexOf(quotableChar) != -1) { + return true; + } + } + return false; + } + } 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 23b45e3f7c16..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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * 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. @@ -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; @@ -169,7 +167,7 @@ public static Class[] parameterInfoToTypes( /** * Create a {@code String[]} representing the argument signature of a * method. Each element in the array is the fully qualified class name - * of the corresponding argument in the methods signature. + * of the corresponding argument in the method's signature. * @param method the method to build an argument signature for * @return the signature as array of argument types */ @@ -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 640bdc81f28f..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, @@ -34,7 +35,7 @@ public class JndiLocatorDelegate extends JndiLocatorSupport { /** * System property that instructs Spring to ignore a default JNDI environment, i.e. * to always return {@code false} from {@link #isDefaultJndiEnvironmentAvailable()}. - *

    The default is "false", allowing for regular default JNDI access e.g. in + *

    The default is "false", allowing for regular default JNDI access, for example, in * {@link JndiPropertySource}. Switching this flag to {@code true} is an optimization * for scenarios where nothing is ever to be found for such JNDI fallback searches * to begin with, avoiding the repeated JNDI lookup overhead. 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 5b901f6c415d..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; /** @@ -30,7 +31,7 @@ * by Jakarta EE applications when accessing a locally mapped (ENC - Environmental * Naming Context) resource. If it doesn't, the "java:comp/env/" prefix will * be prepended if the "resourceRef" property is true (the default is - * false) and no other scheme (e.g. "java:") is given. + * false) and no other scheme (for example, "java:") is given. * * @author Juergen Hoeller * @since 1.1 @@ -51,7 +52,7 @@ public abstract class JndiLocatorSupport extends JndiAccessor { * Set whether the lookup occurs in a Jakarta EE container, i.e. if the prefix * "java:comp/env/" needs to be added if the JNDI name doesn't already * contain it. Default is "false". - *

    Note: Will only get applied if no other scheme (e.g. "java:") is given. + *

    Note: Will only get applied if no other scheme (for example, "java:") is given. */ public void setResourceRef(boolean resourceRef) { this.resourceRef = resourceRef; @@ -117,7 +118,7 @@ protected T lookup(String jndiName, @Nullable Class requiredType) throws /** * Convert the given JNDI name into the actual JNDI name to use. *

    The default implementation applies the "java:comp/env/" prefix if - * "resourceRef" is "true" and no other scheme (e.g. "java:") is given. + * "resourceRef" is "true" and no other scheme (for example, "java:") is given. * @param jndiName the original JNDI name * @return the JNDI name to use * @see #CONTAINER_PREFIX 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 a237e6c731ef..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,18 +35,17 @@ 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; /** * {@link org.springframework.beans.factory.FactoryBean} that looks up a * JNDI object. Exposes the object found in JNDI for bean references, - * e.g. for data access object's "dataSource" property in case of a + * for example, for data access object's "dataSource" property in case of a * {@link javax.sql.DataSource}. * *

    The typical usage will be to register this as singleton factory - * (e.g. for a certain JNDI-bound DataSource) in an application context, + * (for example, for a certain JNDI-bound DataSource) in an application context, * and give bean references to application services that need it. * *

    The default behavior is to look up the JNDI object on startup and cache it. @@ -54,12 +54,12 @@ * a "proxyInterface" in such a scenario, since the actual JNDI object type is not * known in advance. * - *

    Of course, bean classes in a Spring environment may look up e.g. a DataSource + *

    Of course, bean classes in a Spring environment may look up, for example, a DataSource * from JNDI themselves. This class simply enables central configuration of the * JNDI name, and easy switching to non-JNDI alternatives. The latter is * particularly convenient for test setups, reuse in standalone clients, etc. * - *

    Note that switching to e.g. DriverManagerDataSource is just a matter of + *

    Note that switching to, for example, DriverManagerDataSource is just a matter of * configuration: Simply replace the definition of this FactoryBean with a * {@link org.springframework.jdbc.datasource.DriverManagerDataSource} definition! * @@ -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; /** @@ -152,7 +147,7 @@ public void setCache(boolean cache) { *

    Default is "false", i.e. to only expose the JNDI context for object lookup. * Switch this flag to "true" in order to expose the JNDI environment (including * the authorization context) for each method invocation, as needed by WebLogic - * for JNDI-obtained factories (e.g. JDBC DataSource, JMS ConnectionFactory) + * for JNDI-obtained factories (for example, JDBC DataSource, JMS ConnectionFactory) * with authorization requirements. */ public void setExposeAccessContext(boolean exposeAccessContext) { @@ -267,13 +262,12 @@ else if (logger.isDebugEnabled()) { * Return the singleton JNDI object. */ @Override - @Nullable - public Object getObject() { + public @Nullable Object getObject() { return this.jndiObject; } @Override - public Class getObjectType() { + public @Nullable Class getObjectType() { if (this.proxyInterfaces != null) { if (this.proxyInterfaces.length == 1) { return this.proxyInterfaces[0]; @@ -368,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 ff2a0ffb5a0d..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; @@ -32,7 +33,7 @@ * accessing a locally mapped (Environmental Naming Context) resource. If it * doesn't, the "java:comp/env/" prefix will be prepended if the "resourceRef" * property is true (the default is false) and no other scheme - * (e.g. "java:") is given. + * (for example, "java:") is given. * *

    Subclasses may invoke the {@link #lookup()} method whenever it is appropriate. * Some classes might do this on initialization, while others might do it @@ -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 21b1e0cdae65..6afc62f3fd85 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -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()); @@ -148,8 +145,4 @@ public Object getTarget() { } } - @Override - public void releaseTarget(Object target) { - } - } 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 e372ce48c059..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); @@ -127,7 +124,7 @@ public void releaseContext(@Nullable Context ctx) { /** * Create a new JNDI initial context. Invoked by {@link #getContext}. *

    The default implementation use this template's environment settings. - * Can be subclassed for custom contexts, e.g. for testing. + * Can be subclassed for custom contexts, for example, for testing. * @return the initial Context instance * @throws NamingException in case of initialization errors */ 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 00cb01282cc3..07b77bd64c7c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.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,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 @@ -66,17 +66,16 @@ default Clock getClock() { * {@link ScheduledFuture} gets cancelled. * @param task the Runnable to execute whenever the trigger fires * @param trigger an implementation of the {@link Trigger} interface, - * e.g. a {@link org.springframework.scheduling.support.CronTrigger} object + * for example, a {@link org.springframework.scheduling.support.CronTrigger} object * wrapping a cron expression - * @return a {@link ScheduledFuture} representing pending completion of the task, + * @return a {@link ScheduledFuture} representing pending execution of the task, * or {@code null} if the given Trigger object never fires (i.e. returns - * {@code null} from {@link Trigger#nextExecutionTime}) + * {@code null} from {@link Trigger#nextExecution}) * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * 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. @@ -85,9 +84,9 @@ default Clock getClock() { * @param task the Runnable to execute whenever the trigger fires * @param startTime the desired execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @since 5.0 */ ScheduledFuture schedule(Runnable task, Instant startTime); @@ -99,9 +98,9 @@ default Clock getClock() { * @param task the Runnable to execute whenever the trigger fires * @param startTime the desired execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #schedule(Runnable, Instant)} */ @Deprecated(since = "6.0") @@ -118,9 +117,9 @@ default ScheduledFuture schedule(Runnable task, Date startTime) { * @param startTime the desired first execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) * @param period the interval between successive executions of the task - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @since 5.0 */ ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period); @@ -134,9 +133,9 @@ default ScheduledFuture schedule(Runnable task, Date startTime) { * @param startTime the desired first execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) * @param period the interval between successive executions of the task (in milliseconds) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #scheduleAtFixedRate(Runnable, Instant, Duration)} */ @Deprecated(since = "6.0") @@ -151,9 +150,9 @@ default ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, lo * {@link ScheduledFuture} gets cancelled. * @param task the Runnable to execute whenever the trigger fires * @param period the interval between successive executions of the task - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @since 5.0 */ ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period); @@ -165,9 +164,9 @@ default ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, lo * {@link ScheduledFuture} gets cancelled. * @param task the Runnable to execute whenever the trigger fires * @param period the interval between successive executions of the task (in milliseconds) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #scheduleAtFixedRate(Runnable, Duration)} */ @Deprecated(since = "6.0") @@ -185,9 +184,9 @@ default ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { * @param startTime the desired first execution time for the task * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) * @param delay the delay between the completion of one execution and the start of the next - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @since 5.0 */ ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay); @@ -203,9 +202,9 @@ default ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) * @param delay the delay between the completion of one execution and the start of the next * (in milliseconds) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #scheduleWithFixedDelay(Runnable, Instant, Duration)} */ @Deprecated(since = "6.0") @@ -220,9 +219,9 @@ default ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, * {@link ScheduledFuture} gets cancelled. * @param task the Runnable to execute whenever the trigger fires * @param delay the delay between the completion of one execution and the start of the next - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @since 5.0 */ ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay); @@ -235,9 +234,9 @@ default ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, * @param task the Runnable to execute whenever the trigger fires * @param delay the delay between the completion of one execution and the start of the next * (in milliseconds) - * @return a {@link ScheduledFuture} representing pending completion of the task + * @return a {@link ScheduledFuture} representing pending execution of the task * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted - * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @deprecated as of 6.0, in favor of {@link #scheduleWithFixedDelay(Runnable, Duration)} */ @Deprecated(since = "6.0") 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/Async.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java index 5d3c290aa3d1..6d0ee0b4f208 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.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,17 +35,18 @@ *

    In terms of target method signatures, any parameter types are supported. * However, the return type is constrained to either {@code void} or * {@link java.util.concurrent.Future}. In the latter case, you may declare the - * more specific {@link org.springframework.util.concurrent.ListenableFuture} or - * {@link java.util.concurrent.CompletableFuture} types which allow for richer - * interaction with the asynchronous task and for immediate composition with - * further processing steps. + * more specific {@link java.util.concurrent.CompletableFuture} type which allows + * for richer interaction with the asynchronous task and for immediate composition + * with further processing steps. * *

    A {@code Future} handle returned from the proxy will be an actual asynchronous - * {@code Future} that can be used to track the result of the asynchronous method - * execution. However, since the target method needs to implement the same signature, - * it will have to return a temporary {@code Future} handle that just passes a value - * through: for example, Spring's {@link AsyncResult}, EJB 3.1's {@link jakarta.ejb.AsyncResult}, - * or {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}. + * {@code (Completable)Future} that can be used to track the result of the + * asynchronous method execution. However, since the target method needs to implement + * the same signature, it will have to return a temporary {@code Future} handle that + * just passes a value after computation in the execution thread: typically through + * {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}. The + * provided value will be exposed to the caller through the actual asynchronous + * {@code Future} handle at runtime. * * @author Juergen Hoeller * @author Chris Beams 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 6cad78b46dd0..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-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,12 @@ import java.lang.annotation.Annotation; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.Executor; 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; @@ -32,9 +32,9 @@ 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; import org.springframework.util.function.SingletonSupplier; /** @@ -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,9 +92,9 @@ public AsyncAnnotationAdvisor( */ @SuppressWarnings("unchecked") public AsyncAnnotationAdvisor( - @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { + @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { - Set> asyncAnnotationTypes = new LinkedHashSet<>(2); + Set> asyncAnnotationTypes = CollectionUtils.newLinkedHashSet(2); asyncAnnotationTypes.add(Async.class); ClassLoader classLoader = AsyncAnnotationAdvisor.class.getClassLoader(); @@ -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 f20aba5585d4..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-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. @@ -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 bf7a2d0baf63..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,13 +35,12 @@ public class AsyncConfigurerSupport implements AsyncConfigurer { @Override - 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 784a3e3f1ca7..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 @@ -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,23 +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 {@link ListenableFuture}, not just - * plain {@link java.util.concurrent.Future}, along with the corresponding support - * in {@code @Async} processing. - * - *

    As of Spring 4.2, this class also supports passing execution exceptions back - * to the caller. - * * @author Juergen Hoeller * @author Rossen Stoyanchev * @since 3.0 @@ -48,13 +37,11 @@ * @deprecated as of 6.0, in favor of {@link CompletableFuture} */ @Deprecated(since = "6.0") -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; /** @@ -91,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)); @@ -101,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()}. @@ -145,7 +98,7 @@ public CompletableFuture completable() { * @since 4.2 * @see Future#get() */ - public static ListenableFuture forValue(V value) { + public static Future forValue(V value) { return new AsyncResult<>(value, null); } @@ -157,24 +110,8 @@ public static ListenableFuture forValue(V value) { * @since 4.2 * @see ExecutionException */ - public static 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/EnableScheduling.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableScheduling.java index 0d49e240d5df..36e902cab712 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableScheduling.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableScheduling.java @@ -184,7 +184,7 @@ * *

    Note: {@code @EnableScheduling} applies to its local application context only, * allowing for selective scheduling of beans at different levels. Please redeclare - * {@code @EnableScheduling} in each individual context, e.g. the common root web + * {@code @EnableScheduling} 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. * diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index 43436644cad0..e79c7195d76e 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.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. @@ -117,9 +117,9 @@ /** * A time zone for which the cron expression will be resolved. By default, this - * attribute is the empty String (i.e. the server's local time zone will be used). + * attribute is the empty String (i.e. the scheduler's time zone will be used). * @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)}, - * or an empty String to indicate the server's default time zone + * or an empty String to indicate the scheduler's default time zone * @since 4.0 * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone) * @see java.util.TimeZone @@ -127,48 +127,69 @@ String zone() default ""; /** - * Execute the annotated method with a fixed period between the end of the - * last invocation and the start of the next. + * Execute the annotated method with a fixed period between invocations. *

    The time unit is milliseconds by default but can be overridden via * {@link #timeUnit}. - * @return the delay + * @return the period */ - long fixedDelay() default -1; + long fixedRate() default -1; /** - * Execute the annotated method with a fixed period between the end of the - * last invocation and the start of the next. - *

    The time unit is milliseconds by default but can be overridden via - * {@link #timeUnit}. - *

    This attribute variant supports Spring-style "${...}" placeholders - * as well as SpEL expressions. - * @return the delay as a String value — for example, a placeholder - * or a {@link java.time.Duration#parse java.time.Duration} compliant value + * Execute the annotated method with a fixed period between invocations. + *

    The duration String can be in several formats: + *

      + *
    • a plain integer — which is interpreted to represent a duration in + * milliseconds by default unless overridden via {@link #timeUnit()} (prefer + * using {@link #fixedDelay()} in that case)
    • + *
    • any of the known {@link org.springframework.format.annotation.DurationFormat.Style + * DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601} + * style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style + * — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit
    • + *
    • one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions
    • + *
    + * @return the period as a String value — for example a placeholder, + * or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value + * or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value * @since 3.2.2 - * @see #fixedDelay() + * @see #fixedRate() */ - String fixedDelayString() default ""; + String fixedRateString() default ""; /** - * Execute the annotated method with a fixed period between invocations. + * Execute the annotated method with a fixed period between the end of the + * last invocation and the start of the next. *

    The time unit is milliseconds by default but can be overridden via * {@link #timeUnit}. - * @return the period + *

    NOTE: With virtual threads, fixed rates and cron triggers are recommended + * over fixed delays. Fixed-delay tasks operate on a single scheduler thread + * with {@link org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler}. + * @return the delay */ - long fixedRate() default -1; + long fixedDelay() default -1; /** - * Execute the annotated method with a fixed period between invocations. - *

    The time unit is milliseconds by default but can be overridden via - * {@link #timeUnit}. - *

    This attribute variant supports Spring-style "${...}" placeholders - * as well as SpEL expressions. - * @return the period as a String value — for example, a placeholder - * or a {@link java.time.Duration#parse java.time.Duration} compliant value + * Execute the annotated method with a fixed period between the end of the + * last invocation and the start of the next. + *

    The duration String can be in several formats: + *

      + *
    • a plain integer — which is interpreted to represent a duration in + * milliseconds by default unless overridden via {@link #timeUnit()} (prefer + * using {@link #fixedDelay()} in that case)
    • + *
    • any of the known {@link org.springframework.format.annotation.DurationFormat.Style + * DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601} + * style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style + * — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit
    • + *
    + *

    NOTE: With virtual threads, fixed rates and cron triggers are recommended + * over fixed delays. Fixed-delay tasks operate on a single scheduler thread + * with {@link org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler}. + * @return the delay as a String value — for example a placeholder, + * or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value + * or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value * @since 3.2.2 - * @see #fixedRate() + * @see #fixedDelay() */ - String fixedRateString() default ""; + String fixedDelayString() default ""; /** * Number of units of time to delay before the first execution of a @@ -183,12 +204,20 @@ /** * Number of units of time to delay before the first execution of a * {@link #fixedRate} or {@link #fixedDelay} task. - *

    The time unit is milliseconds by default but can be overridden via - * {@link #timeUnit}. - *

    This attribute variant supports Spring-style "${...}" placeholders - * as well as SpEL expressions. - * @return the initial delay as a String value — for example, a placeholder - * or a {@link java.time.Duration#parse java.time.Duration} compliant value + *

    The duration String can be in several formats: + *

      + *
    • a plain integer — which is interpreted to represent a duration in + * milliseconds by default unless overridden via {@link #timeUnit()} (prefer + * using {@link #fixedDelay()} in that case)
    • + *
    • any of the known {@link org.springframework.format.annotation.DurationFormat.Style + * DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601} + * style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style + * — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit
    • + *
    • one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions
    • + *
    + * @return the initial delay as a String value — for example a placeholder, + * or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value + * or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value * @since 3.2.2 * @see #initialDelay() */ 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 dd2ee567191d..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 @@ -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,7 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; @@ -34,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; @@ -45,6 +45,7 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; @@ -59,7 +60,8 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; +import org.springframework.format.annotation.DurationFormat; +import org.springframework.format.datetime.standard.DurationFormatterUtils; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.config.CronTask; @@ -89,7 +91,7 @@ * *

    Autodetects any {@link SchedulingConfigurer} instances in the container, * allowing for customization of the scheduler to be used or for fine-grained - * control over task registration (e.g. registration of {@link Trigger} tasks). + * control over task registration (for example, registration of {@link Trigger} tasks). * See the {@link EnableScheduling @EnableScheduling} javadocs for complete usage * details. * @@ -132,30 +134,26 @@ 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 = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); + private final Set> nonAnnotatedClasses = ConcurrentHashMap.newKeySet(64); private final Map> scheduledTasks = new IdentityHashMap<>(16); private final Map> reactiveSubscriptions = new IdentityHashMap<>(16); + private final Set manualCancellationOnContextClose = Collections.newSetFromMap(new IdentityHashMap<>(16)); + /** * Create a default {@code ScheduledAnnotationBeanPostProcessor}. @@ -306,6 +304,13 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + "': " + annotatedMethods); } + if ((this.beanFactory != null && + (!this.beanFactory.containsBean(beanName) || !this.beanFactory.isSingleton(beanName)) || + (this.beanFactory instanceof SingletonBeanRegistry sbr && sbr.containsSingleton(beanName)))) { + // Either a prototype/scoped bean or a FactoryBean with a pre-existing managed singleton + // -> trigger manual cancellation when ContextClosedEvent comes in + this.manualCancellationOnContextClose.add(bean); + } } } return bean; @@ -412,7 +417,7 @@ private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method } catch (RuntimeException ex) { throw new IllegalArgumentException( - "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long"); + "Invalid initialDelayString value \"" + initialDelayString + "\"; " + ex); } } } @@ -429,14 +434,14 @@ private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method Assert.isTrue(initialDelay.isNegative(), "'initialDelay' not supported for cron triggers"); processedSchedule = true; if (!Scheduled.CRON_DISABLED.equals(cron)) { - TimeZone timeZone; + CronTrigger trigger; if (StringUtils.hasText(zone)) { - timeZone = StringUtils.parseTimeZoneString(zone); + trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone)); } else { - timeZone = TimeZone.getDefault(); + trigger = new CronTrigger(cron); } - tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); + tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, trigger))); } } } @@ -464,7 +469,7 @@ private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method } catch (RuntimeException ex) { throw new IllegalArgumentException( - "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long"); + "Invalid fixedDelayString value \"" + fixedDelayString + "\"; " + ex); } tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse))); } @@ -490,7 +495,7 @@ private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method } catch (RuntimeException ex) { throw new IllegalArgumentException( - "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long"); + "Invalid fixedRateString value \"" + fixedRateString + "\"; " + ex); } tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse))); } @@ -543,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; } @@ -559,21 +563,10 @@ private static Duration toDuration(long value, TimeUnit timeUnit) { } private static Duration toDuration(String value, TimeUnit timeUnit) { - if (isDurationString(value)) { - return Duration.parse(value); - } - return toDuration(Long.parseLong(value), timeUnit); - } - - private static boolean isDurationString(String value) { - return (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1)))); - } - - private static boolean isP(char ch) { - return (ch == 'P' || ch == 'p'); + DurationFormat.Unit unit = DurationFormat.Unit.fromChronoUnit(timeUnit.toChronoUnit()); + return DurationFormatterUtils.detectAndParse(value, unit); // interpreting as long as fallback already } - /** * Return all currently scheduled tasks, from {@link Scheduled} methods * as well as from programmatic {@link SchedulingConfigurer} interaction. @@ -596,6 +589,18 @@ public Set getScheduledTasks() { @Override public void postProcessBeforeDestruction(Object bean, String beanName) { + cancelScheduledTasks(bean); + this.manualCancellationOnContextClose.remove(bean); + } + + @Override + public boolean requiresDestruction(Object bean) { + synchronized (this.scheduledTasks) { + return (this.scheduledTasks.containsKey(bean) || this.reactiveSubscriptions.containsKey(bean)); + } + } + + private void cancelScheduledTasks(Object bean) { Set tasks; List liveSubscriptions; synchronized (this.scheduledTasks) { @@ -614,13 +619,6 @@ public void postProcessBeforeDestruction(Object bean, String beanName) { } } - @Override - public boolean requiresDestruction(Object bean) { - synchronized (this.scheduledTasks) { - return (this.scheduledTasks.containsKey(bean) || this.reactiveSubscriptions.containsKey(bean)); - } - } - @Override public void destroy() { synchronized (this.scheduledTasks) { @@ -637,7 +635,10 @@ public void destroy() { liveSubscription.run(); // equivalent to cancelling the subscription } } + this.reactiveSubscriptions.clear(); + this.manualCancellationOnContextClose.clear(); } + this.registrar.destroy(); if (this.localScheduler != null) { this.localScheduler.destroy(); @@ -656,19 +657,14 @@ public void onApplicationEvent(ApplicationContextEvent event) { if (event instanceof ContextRefreshedEvent) { // Running in an ApplicationContext -> register tasks this late... // giving other ContextRefreshedEvent listeners a chance to perform - // their work at the same time (e.g. Spring Batch's job registration). + // their work at the same time (for example, Spring Batch's job registration). finishRegistration(); } else if (event instanceof ContextClosedEvent) { - synchronized (this.scheduledTasks) { - Collection> allTasks = this.scheduledTasks.values(); - for (Set tasks : allTasks) { - for (ScheduledTask task : tasks) { - // At this early point, let in-progress tasks complete still - task.cancel(false); - } - } + for (Object bean : this.manualCancellationOnContextClose) { + cancelScheduledTasks(bean); } + this.manualCancellationOnContextClose.clear(); } } } 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 3f6b1696433b..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-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import 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, @@ -123,8 +123,9 @@ public static Runnable createSubscriptionRunnable(Method method, Object targetBe Publisher publisher = getPublisherFor(method, targetBean); Supplier contextSupplier = () -> new ScheduledTaskObservationContext(targetBean, method); + String displayName = targetBean.getClass().getName() + "." + method.getName(); return new SubscribingRunnable(publisher, shouldBlock, scheduled.scheduler(), - subscriptionTrackerRegistry, observationRegistrySupplier, contextSupplier); + subscriptionTrackerRegistry, displayName, observationRegistrySupplier, contextSupplier); } /** @@ -137,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()); } @@ -192,8 +193,9 @@ static final class SubscribingRunnable implements SchedulingAwareRunnable { final boolean shouldBlock; - @Nullable - private final String qualifier; + final String displayName; + + private final @Nullable String qualifier; private final List subscriptionTrackerRegistry; @@ -203,11 +205,12 @@ static final class SubscribingRunnable implements SchedulingAwareRunnable { SubscribingRunnable(Publisher publisher, boolean shouldBlock, @Nullable String qualifier, List subscriptionTrackerRegistry, - Supplier observationRegistrySupplier, + String displayName, Supplier observationRegistrySupplier, Supplier contextSupplier) { this.publisher = publisher; this.shouldBlock = shouldBlock; + this.displayName = displayName; this.qualifier = qualifier; this.subscriptionTrackerRegistry = subscriptionTrackerRegistry; this.observationRegistrySupplier = observationRegistrySupplier; @@ -215,8 +218,7 @@ static final class SubscribingRunnable implements SchedulingAwareRunnable { } @Override - @Nullable - public String getQualifier() { + public @Nullable String getQualifier() { return this.qualifier; } @@ -232,7 +234,7 @@ public void run() { latch.await(); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } } else { @@ -244,6 +246,7 @@ public void run() { private void subscribe(TrackingSubscriber subscriber, Observation observation) { this.subscriptionTrackerRegistry.add(subscriber); if (reactorPresent) { + observation.start(); Flux.from(this.publisher) .contextWrite(context -> context.put(ObservationThreadLocalAccessor.KEY, observation)) .subscribe(subscriber); @@ -252,6 +255,11 @@ private void subscribe(TrackingSubscriber subscriber, Observation observation) { this.publisher.subscribe(subscriber); } } + + @Override + public String toString() { + return this.displayName; + } } @@ -266,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); @@ -300,7 +306,6 @@ public void run() { @Override public void onSubscribe(Subscription subscription) { this.subscription = subscription; - this.observation.start(); subscription.request(Integer.MAX_VALUE); } 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 1edebb80de73..322ced233ae9 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-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. @@ -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 @@ -63,14 +62,13 @@ * @see ThreadPoolTaskExecutor */ @SuppressWarnings("deprecation") -public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { +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; /** @@ -143,11 +140,6 @@ public final Executor getConcurrentExecutor() { * execution callback (which may be a wrapper around the user-supplied task). *

    The primary use case is to set some execution context around the task's * invocation, or to provide some monitoring/statistics for task execution. - *

    NOTE: Exception handling in {@code TaskDecorator} implementations - * is limited to plain {@code Runnable} execution via {@code execute} calls. - * In case of {@code #submit} calls, the exposed {@code Runnable} will be a - * {@code FutureTask} which does not propagate any exceptions; you might - * have to cast it and call {@code Future#get} to evaluate exceptions. * @since 4.3 */ public final void setTaskDecorator(TaskDecorator taskDecorator) { @@ -177,28 +169,21 @@ 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 concurrentExecutor) { - if (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(concurrentExecutor)) { - return new ManagedTaskExecutorAdapter(concurrentExecutor); - } - TaskExecutorAdapter adapter = new TaskExecutorAdapter(concurrentExecutor); + private TaskExecutorAdapter getAdaptedExecutor(Executor originalExecutor) { + TaskExecutorAdapter adapter = + (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(originalExecutor) ? + new ManagedTaskExecutorAdapter(originalExecutor) : new TaskExecutorAdapter(originalExecutor)); if (this.taskDecorator != null) { adapter.setTaskDecorator(this.taskDecorator); } return adapter; } + Runnable decorateTaskIfNecessary(Runnable task) { + return (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task); + } + /** * TaskExecutorAdapter subclass that wraps all provided Runnables and Callables @@ -226,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 88f36184efd9..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 @@ -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,8 +20,10 @@ import java.time.Duration; import java.time.Instant; import java.util.Date; +import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -29,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; @@ -73,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 { @@ -89,12 +90,11 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T } - private ScheduledExecutorService scheduledExecutor; + private @Nullable ScheduledExecutorService scheduledExecutor; private boolean enterpriseConcurrentScheduler = false; - @Nullable - private ErrorHandler errorHandler; + private @Nullable ErrorHandler errorHandler; private Clock clock = Clock.systemDefaultZone(); @@ -168,6 +168,13 @@ public void setScheduledExecutor(ScheduledExecutorService scheduledExecutor) { initScheduledExecutor(scheduledExecutor); } + private ScheduledExecutorService getScheduledExecutor() { + if (this.scheduledExecutor == null) { + throw new IllegalStateException("No ScheduledExecutor is configured"); + } + return this.scheduledExecutor; + } + /** * Provide an {@link ErrorHandler} strategy. */ @@ -183,6 +190,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -193,8 +201,23 @@ public Clock getClock() { @Override - @Nullable - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public void execute(Runnable task) { + super.execute(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } + + @Override + public Future submit(Runnable task) { + return super.submit(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } + + @Override + public Future submit(Callable task) { + return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); + } + + @Override + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); try { if (this.enterpriseConcurrentScheduler) { return new EnterpriseConcurrentTriggerScheduler().schedule(decorateTask(task, true), trigger); @@ -202,73 +225,81 @@ public ScheduledFuture schedule(Runnable task, Trigger trigger) { else { ErrorHandler errorHandler = (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true)); - return new ReschedulingRunnable(task, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule(); + return new ReschedulingRunnable( + decorateTaskIfNecessary(task), trigger, this.clock, scheduleExecutorToUse, errorHandler) + .schedule(); } } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture schedule(Runnable task, Instant startTime) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); Duration delay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.schedule(decorateTask(task, false), NANO.convert(delay), NANO); + return scheduleExecutorToUse.schedule(decorateTask(task, false), NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), + return scheduleExecutorToUse.scheduleAtFixedRate(decorateTask(task, true), NANO.convert(initialDelay), NANO.convert(period), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); try { - return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), + return scheduleExecutorToUse.scheduleAtFixedRate(decorateTask(task, true), 0, NANO.convert(period), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), + return scheduleExecutorToUse.scheduleWithFixedDelay(decorateTask(task, true), NANO.convert(initialDelay), NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } @Override public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { + ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); try { - return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), + return scheduleExecutorToUse.scheduleWithFixedDelay(decorateTask(task, true), 0, NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(scheduleExecutorToUse, task, ex); } } private Runnable decorateTask(Runnable task, boolean isRepeatingTask) { Runnable result = TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, isRepeatingTask); + result = decorateTaskIfNecessary(result); if (this.enterpriseConcurrentScheduler) { result = ManagedTaskBuilder.buildManagedTask(result, task.toString()); } @@ -283,7 +314,7 @@ private Runnable decorateTask(Runnable task, boolean isRepeatingTask) { private class EnterpriseConcurrentTriggerScheduler { public ScheduledFuture schedule(Runnable task, Trigger trigger) { - ManagedScheduledExecutorService executor = (ManagedScheduledExecutorService) scheduledExecutor; + ManagedScheduledExecutorService executor = (ManagedScheduledExecutorService) getScheduledExecutor(); return executor.schedule(task, new TriggerAdapter(trigger)); } @@ -297,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); } @@ -311,30 +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 - public Instant lastScheduledExecution() { + public @Nullable Instant lastScheduledExecution() { return (this.le != null ? toInstant(this.le.getScheduledStart()) : null); } @Override - public Instant lastActualExecution() { + public @Nullable Instant lastActualExecution() { return (this.le != null ? toInstant(this.le.getRunStart()) : null); } @Override - 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/DefaultManagedTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java index 5ac8ead64e51..b41315f86d55 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java @@ -28,6 +28,8 @@ /** * JNDI-based variant of {@link ConcurrentTaskScheduler}, performing a default lookup for * JSR-236's "java:comp/DefaultManagedScheduledExecutorService" in a Jakarta EE environment. + * Expected to be exposed as a bean, in particular as the default lookup happens in the + * standard {@link InitializingBean#afterPropertiesSet()} callback. * *

    Note: This class is not strictly JSR-236 based; it can work with any regular * {@link java.util.concurrent.ScheduledExecutorService} that can be found in JNDI. 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 new file mode 100644 index 000000000000..fd2c3ac3f8aa --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DelegatingErrorHandlingCallable.java @@ -0,0 +1,65 @@ +/* + * 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.concurrent; + +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.Callable; + +import org.jspecify.annotations.Nullable; + +import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.ErrorHandler; +import org.springframework.util.ReflectionUtils; + +/** + * {@link Callable} adapter for an {@link ErrorHandler}. + * + * @author Juergen Hoeller + * @since 6.2 + * @param the value type + */ +class DelegatingErrorHandlingCallable implements Callable { + + private final Callable delegate; + + private final ErrorHandler errorHandler; + + + public DelegatingErrorHandlingCallable(Callable delegate, @Nullable ErrorHandler errorHandler) { + this.delegate = delegate; + this.errorHandler = (errorHandler != null ? errorHandler : + TaskUtils.getDefaultErrorHandler(false)); + } + + + @Override + public @Nullable V call() throws Exception { + try { + return this.delegate.call(); + } + catch (Throwable ex) { + try { + this.errorHandler.handleError(ex); + } + catch (UndeclaredThrowableException exToPropagate) { + ReflectionUtils.rethrowException(exToPropagate.getUndeclaredThrowable()); + } + return null; + } + } + +} 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 5d69b55d6532..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 @@ -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,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; @@ -33,9 +34,11 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; +import org.springframework.context.Lifecycle; import org.springframework.context.SmartLifecycle; import org.springframework.context.event.ContextClosedEvent; -import org.springframework.lang.Nullable; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.VirtualThreadTaskExecutor; /** * Base class for setting up a {@link java.util.concurrent.ExecutorService} @@ -58,8 +61,24 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac implements BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle, ApplicationListener { + /** + * The default phase for an executor {@link SmartLifecycle}: {@code Integer.MAX_VALUE / 2}. + *

    This is different from the default phase {@code Integer.MAX_VALUE} associated with + * other {@link SmartLifecycle} implementations, putting the typically auto-started + * executor/scheduler beans into an earlier startup phase and a later shutdown phase while + * still leaving room for regular {@link Lifecycle} components with the common phase 0. + * @since 6.2 + * @see #getPhase() + * @see SmartLifecycle#DEFAULT_PHASE + * @see org.springframework.context.support.DefaultLifecycleProcessor#setTimeoutPerShutdownPhase + */ + public static final int DEFAULT_PHASE = Integer.MAX_VALUE / 2; + + protected final Log logger = LogFactory.getLog(getClass()); + private boolean virtualThreads = false; + private ThreadFactory threadFactory = this; private boolean threadNamePrefixSet = false; @@ -74,22 +93,39 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac private int phase = DEFAULT_PHASE; - @Nullable - private String beanName; + private @Nullable String beanName; + + private @Nullable ApplicationContext applicationContext; + + private @Nullable ExecutorService executor; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ExecutorLifecycleDelegate lifecycleDelegate; - @Nullable - private ExecutorService executor; + private volatile boolean lateShutdown; - @Nullable - private ExecutorLifecycleDelegate lifecycleDelegate; + /** + * Specify whether to use virtual threads instead of platform threads. + * This is off by default, setting up a traditional platform thread pool. + *

    Set this flag to {@code true} on Java 21 or higher for a tightly + * managed thread pool setup with virtual threads. In contrast to + * {@link SimpleAsyncTaskExecutor}, this is integrated with Spring's + * lifecycle management for stopping and restarting execution threads, + * including an early stop signal for a graceful shutdown arrangement. + *

    Specify either this or {@link #setThreadFactory}, not both. + * @since 6.2 + * @see #setThreadFactory + * @see VirtualThreadTaskExecutor#getVirtualThreadFactory() + * @see SimpleAsyncTaskExecutor#setVirtualThreads + */ + public void setVirtualThreads(boolean virtualThreads) { + this.virtualThreads = virtualThreads; + this.threadFactory = this; + } /** * Set the ThreadFactory to use for the ExecutorService's thread pool. - * Default is the underlying ExecutorService's default thread factory. + * The default is the underlying ExecutorService's default thread factory. *

    In a Jakarta EE or other managed environment with JSR-236 support, * consider specifying a JNDI-located ManagedThreadFactory: by default, * to be found at "java:comp/DefaultManagedThreadFactory". @@ -103,6 +139,7 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac */ public void setThreadFactory(@Nullable ThreadFactory threadFactory) { this.threadFactory = (threadFactory != null ? threadFactory : this); + this.virtualThreads = false; } @Override @@ -113,7 +150,7 @@ public void setThreadNamePrefix(@Nullable String threadNamePrefix) { /** * Set the RejectedExecutionHandler to use for the ExecutorService. - * Default is the ExecutorService's default abort policy. + * The default is the ExecutorService's default abort policy. * @see java.util.concurrent.ThreadPoolExecutor.AbortPolicy */ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejectedExecutionHandler) { @@ -124,18 +161,29 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec /** * Set whether to accept further tasks after the application context close phase * has begun. - *

    Default is {@code false} as of 6.1, triggering an early soft shutdown of + *

    The default is {@code false} as of 6.1, triggering an early soft shutdown of * the executor and therefore rejecting any further task submissions. Switch this * to {@code true} in order to let other components submit tasks even during their - * own destruction callbacks, at the expense of a longer shutdown phase. - * This will usually go along with - * {@link #setWaitForTasksToCompleteOnShutdown "waitForTasksToCompleteOnShutdown"}. + * own stop and destruction callbacks, at the expense of a longer shutdown phase. + * The executor will not go through a coordinated lifecycle stop phase then + * but rather only stop tasks on its own shutdown. + *

    {@code acceptTasksAfterContextClose=true} like behavior also follows from + * {@link #setWaitForTasksToCompleteOnShutdown "waitForTasksToCompleteOnShutdown"} + * which effectively is a specific variant of this flag, replacing the early soft + * shutdown in the concurrent managed stop phase with a serial soft shutdown in + * the executor's destruction step, with individual awaiting according to the + * {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} property. *

    This flag will only have effect when the executor is running in a Spring - * application context and able to receive the {@link ContextClosedEvent}. + * application context and able to receive the {@link ContextClosedEvent}. Also, + * note that {@link ThreadPoolTaskExecutor} effectively accepts tasks after context + * close by default, in combination with a coordinated lifecycle stop, unless + * {@link ThreadPoolTaskExecutor#setStrictEarlyShutdown "strictEarlyShutdown"} + * has been specified. * @since 6.1 * @see org.springframework.context.ConfigurableApplicationContext#close() * @see DisposableBean#destroy() * @see #shutdown() + * @see #setAwaitTerminationSeconds */ public void setAcceptTasksAfterContextClose(boolean acceptTasksAfterContextClose) { this.acceptTasksAfterContextClose = acceptTasksAfterContextClose; @@ -144,17 +192,23 @@ public void setAcceptTasksAfterContextClose(boolean acceptTasksAfterContextClose /** * Set whether to wait for scheduled tasks to complete on shutdown, * not interrupting running tasks and executing all tasks in the queue. - *

    Default is {@code false}, shutting down immediately through interrupting - * ongoing tasks and clearing the queue. Switch this flag to {@code true} if - * you prefer fully completed tasks at the expense of a longer shutdown phase. + *

    The default is {@code false}, with a coordinated lifecycle stop first + * (unless {@link #setAcceptTasksAfterContextClose "acceptTasksAfterContextClose"} + * has been set) and then an immediate shutdown through interrupting ongoing + * tasks and clearing the queue. Switch this flag to {@code true} if you + * prefer fully completed tasks at the expense of a longer shutdown phase. + * The executor will not go through a coordinated lifecycle stop phase then + * but rather only stop and wait for task completion on its own shutdown. *

    Note that Spring's container shutdown continues while ongoing tasks * are being completed. If you want this executor to block and wait for the * termination of tasks before the rest of the container continues to shut - * down - e.g. in order to keep up other resources that your tasks may need -, + * down - for example, in order to keep up other resources that your tasks may need -, * set the {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} * property instead of or in addition to this property. * @see java.util.concurrent.ExecutorService#shutdown() * @see java.util.concurrent.ExecutorService#shutdownNow() + * @see #shutdown() + * @see #setAwaitTerminationSeconds */ public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; @@ -199,7 +253,8 @@ public void setAwaitTerminationMillis(long awaitTerminationMillis) { /** * Specify the lifecycle phase for pausing and resuming this executor. - * The default is {@link #DEFAULT_PHASE}. + *

    The default for executors/schedulers is {@link #DEFAULT_PHASE} as of 6.2, + * for stopping after other {@link SmartLifecycle} implementations. * @since 6.1 * @see SmartLifecycle#getPhase() */ @@ -247,7 +302,9 @@ public void initialize() { if (!this.threadNamePrefixSet && this.beanName != null) { setThreadNamePrefix(this.beanName + "-"); } - this.executor = initializeExecutor(this.threadFactory, this.rejectedExecutionHandler); + ThreadFactory factory = (this.virtualThreads ? + new VirtualThreadTaskExecutor(getThreadNamePrefix()).getVirtualThreadFactory() : this.threadFactory); + this.executor = initializeExecutor(factory, this.rejectedExecutionHandler); this.lifecycleDelegate = new ExecutorLifecycleDelegate(this.executor); } @@ -279,6 +336,9 @@ public void destroy() { * scheduling of periodic tasks, letting existing tasks complete still. * This step is non-blocking and can be applied as an early shutdown signal * before following up with a full {@link #shutdown()} call later on. + *

    Automatically called for early shutdown signals on + * {@link #onApplicationEvent(ContextClosedEvent) context close}. + * Can be manually called as well, in particular outside a container. * @since 6.1 * @see #shutdown() * @see java.util.concurrent.ExecutorService#shutdown() @@ -319,7 +379,7 @@ public void shutdown() { } /** - * Cancel the given remaining task which never commended execution, + * Cancel the given remaining task which never commenced execution, * as returned from {@link ExecutorService#shutdownNow()}. * @param task the task to cancel (typically a {@link RunnableFuture}) * @since 5.0.5 @@ -374,7 +434,7 @@ public void start() { */ @Override public void stop() { - if (this.lifecycleDelegate != null) { + if (this.lifecycleDelegate != null && !this.lateShutdown) { this.lifecycleDelegate.stop(); } } @@ -386,9 +446,12 @@ public void stop() { */ @Override public void stop(Runnable callback) { - if (this.lifecycleDelegate != null) { + if (this.lifecycleDelegate != null && !this.lateShutdown) { this.lifecycleDelegate.stop(callback); } + else { + callback.run(); + } } /** @@ -439,11 +502,35 @@ protected void afterExecute(Runnable task, @Nullable Throwable ex) { */ @Override public void onApplicationEvent(ContextClosedEvent event) { - if (event.getApplicationContext() == this.applicationContext && !this.acceptTasksAfterContextClose) { - // Early shutdown signal: accept no further tasks, let existing tasks complete - // before hitting the actual destruction step in the shutdown() method above. - initiateShutdown(); + if (event.getApplicationContext() == this.applicationContext) { + if (this.acceptTasksAfterContextClose || this.waitForTasksToCompleteOnShutdown) { + // Late shutdown without early stop lifecycle. + this.lateShutdown = true; + } + else { + if (this.lifecycleDelegate != null) { + this.lifecycleDelegate.markShutdown(); + } + initiateEarlyShutdown(); + } } } + /** + * Early shutdown signal: do not trigger further tasks, let existing tasks complete + * before hitting the actual destruction step in the {@link #shutdown()} method. + * This goes along with a {@link #stop(Runnable) coordinated lifecycle stop phase}. + *

    Called from {@link #onApplicationEvent(ContextClosedEvent)} if no + * indications for a late shutdown have been determined, that is, if the + * {@link #setAcceptTasksAfterContextClose "acceptTasksAfterContextClose} and + * {@link #setWaitForTasksToCompleteOnShutdown "waitForTasksToCompleteOnShutdown"} + * flags have not been set. + *

    The default implementation calls {@link #initiateShutdown()}. + * @since 6.1.4 + * @see #initiateShutdown() + */ + protected void initiateEarlyShutdown() { + initiateShutdown(); + } + } 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 3f449f12bdc9..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 @@ -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,10 +18,12 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.locks.Condition; +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 @@ -36,16 +38,17 @@ final class ExecutorLifecycleDelegate implements SmartLifecycle { private final ExecutorService executor; - private final ReentrantLock pauseLock = new ReentrantLock(); + private final Lock pauseLock = new ReentrantLock(); private final Condition unpaused = this.pauseLock.newCondition(); private volatile boolean paused; + private volatile boolean shutdown; + private int executingTaskCount = 0; - @Nullable - private Runnable stopCallback; + private @Nullable Runnable stopCallback; public ExecutorLifecycleDelegate(ExecutorService executor) { @@ -97,13 +100,17 @@ public void stop(Runnable callback) { @Override public boolean isRunning() { - return (!this.executor.isShutdown() & !this.paused); + return (!this.paused && !this.executor.isTerminated()); + } + + void markShutdown() { + this.shutdown = true; } void beforeExecute(Thread thread) { this.pauseLock.lock(); try { - while (this.paused && !this.executor.isShutdown()) { + while (this.paused && !this.shutdown && !this.executor.isShutdown()) { this.unpaused.await(); } } 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 5e69c5768dbc..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 @@ -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,7 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; @@ -27,6 +28,9 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; 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; import org.springframework.context.ApplicationListener; @@ -34,10 +38,11 @@ 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; import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; /** @@ -46,7 +51,7 @@ * separate thread. This is an attractive choice with virtual threads on JDK 21, * expecting common usage with {@link #setVirtualThreads setVirtualThreads(true)}. * - *

    NOTE: Scheduling with a fixed delay enforces execution on the single + *

    NOTE: Scheduling with a fixed delay enforces execution on a single * scheduler thread, in order to provide traditional fixed-delay semantics! * Prefer the use of fixed rates or cron triggers instead which are a better fit * with this thread-per-task scheduler variant. @@ -64,15 +69,23 @@ * consider setting {@link #setVirtualThreads} to {@code true}. * *

    Extends {@link SimpleAsyncTaskExecutor} and can serve as a fully capable - * replacement for it, e.g. as a single shared instance serving as a + * replacement for it, for example, as a single shared instance serving as a * {@link org.springframework.core.task.TaskExecutor} as well as a {@link TaskScheduler}. * This is generally not the case with other executor/scheduler implementations * which tend to have specific constraints for the scheduler thread pool, * requiring a separate thread pool for general executor purposes in practice. * + *

    NOTE: This scheduler variant does not track the actual completion of tasks + * but rather just the hand-off to an execution thread. As a consequence, + * a {@link ScheduledFuture} handle (for example, from {@link #schedule(Runnable, Instant)}) + * represents that hand-off rather than the actual completion of the provided task + * (or series of repeated tasks). Also, this scheduler participates in lifecycle + * management to a limited degree only, stopping trigger firing and fixed-delay + * task execution but not stopping the execution of handed-off tasks. + * *

    As an alternative to the built-in thread-per-task capability, this scheduler * can also be configured with a separate target executor for scheduled task - * execution through {@link #setTargetTaskExecutor}: e.g. pointing to a shared + * execution through {@link #setTargetTaskExecutor}: for example, pointing to a shared * {@link ThreadPoolTaskExecutor} bean. This is still rather different from a * {@link ThreadPoolTaskScheduler} setup since it always uses a single scheduler * thread while dynamically dispatching to the target thread pool which may have @@ -90,31 +103,52 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements TaskScheduler, ApplicationContextAware, SmartLifecycle, ApplicationListener { + /** + * The default phase for an executor {@link SmartLifecycle}: {@code Integer.MAX_VALUE / 2}. + * @since 6.2 + * @see #getPhase() + * @see ExecutorConfigurationSupport#DEFAULT_PHASE + */ + public static final int DEFAULT_PHASE = ExecutorConfigurationSupport.DEFAULT_PHASE; + private static final TimeUnit NANO = TimeUnit.NANOSECONDS; - private final ScheduledExecutorService scheduledExecutor = createScheduledExecutor(); + private final ScheduledExecutorService triggerExecutor = createScheduledExecutor(); + + private final ExecutorLifecycleDelegate triggerLifecycle = new ExecutorLifecycleDelegate(this.triggerExecutor); - private final ExecutorLifecycleDelegate lifecycleDelegate = new ExecutorLifecycleDelegate(this.scheduledExecutor); + private final ScheduledExecutorService fixedDelayExecutor = createFixedDelayExecutor(); + + private final ExecutorLifecycleDelegate fixedDelayLifecycle = new ExecutorLifecycleDelegate(this.fixedDelayExecutor); + + private @Nullable ErrorHandler errorHandler; private Clock clock = Clock.systemDefaultZone(); private int phase = DEFAULT_PHASE; - @Nullable - private Executor targetTaskExecutor; + private @Nullable Executor targetTaskExecutor; + + private @Nullable ApplicationContext applicationContext; - @Nullable - private ApplicationContext applicationContext; + /** + * Provide an {@link ErrorHandler} strategy. + * @since 6.2 + */ + public void setErrorHandler(ErrorHandler errorHandler) { + Assert.notNull(errorHandler, "ErrorHandler must not be null"); + this.errorHandler = errorHandler; + } /** * Set the clock to use for scheduling purposes. *

    The default clock is the system clock for the default time zone. - * @since 5.3 * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -163,11 +197,24 @@ private ScheduledExecutorService createScheduledExecutor() { return new ScheduledThreadPoolExecutor(1, this::newThread) { @Override protected void beforeExecute(Thread thread, Runnable task) { - lifecycleDelegate.beforeExecute(thread); + triggerLifecycle.beforeExecute(thread); + } + @Override + protected void afterExecute(Runnable task, Throwable ex) { + triggerLifecycle.afterExecute(); + } + }; + } + + private ScheduledExecutorService createFixedDelayExecutor() { + return new ScheduledThreadPoolExecutor(1, this::newThread) { + @Override + protected void beforeExecute(Thread thread, Runnable task) { + fixedDelayLifecycle.beforeExecute(thread); } @Override protected void afterExecute(Runnable task, Throwable ex) { - lifecycleDelegate.afterExecute(); + fixedDelayLifecycle.afterExecute(); } }; } @@ -182,22 +229,54 @@ protected void doExecute(Runnable task) { } } + private Runnable taskOnSchedulerThread(Runnable task) { + return new DelegatingErrorHandlingRunnable(task, + (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true))); + } + private Runnable scheduledTask(Runnable task) { - return () -> execute(task); + return () -> execute(new DelegatingErrorHandlingRunnable(task, this::shutdownAwareErrorHandler)); } + private void shutdownAwareErrorHandler(Throwable ex) { + if (this.errorHandler != null) { + this.errorHandler.handleError(ex); + } + else if (this.triggerExecutor.isShutdown()) { + LogFactory.getLog(getClass()).debug("Ignoring scheduled task exception after shutdown", ex); + } + else { + TaskUtils.getDefaultErrorHandler(true).handleError(ex); + } + } + + + @Override + public void execute(Runnable task) { + super.execute(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } @Override - @Nullable - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public Future submit(Runnable task) { + return super.submit(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } + + @Override + public Future submit(Callable task) { + return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); + } + + @Override + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { try { Runnable delegate = scheduledTask(task); - ErrorHandler errorHandler = TaskUtils.getDefaultErrorHandler(true); + ErrorHandler errorHandler = + (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true)); return new ReschedulingRunnable( - delegate, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule(); + delegate, trigger, this.clock, this.triggerExecutor, errorHandler).schedule(); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(this.triggerExecutor, task, ex); } } @@ -205,10 +284,10 @@ public ScheduledFuture schedule(Runnable task, Trigger trigger) { public ScheduledFuture schedule(Runnable task, Instant startTime) { Duration delay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.schedule(scheduledTask(task), NANO.convert(delay), NANO); + return this.triggerExecutor.schedule(scheduledTask(task), NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(this.triggerExecutor, task, ex); } } @@ -216,22 +295,22 @@ public ScheduledFuture schedule(Runnable task, Instant startTime) { public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { - return this.scheduledExecutor.scheduleAtFixedRate(scheduledTask(task), + return this.triggerExecutor.scheduleAtFixedRate(scheduledTask(task), NANO.convert(initialDelay), NANO.convert(period), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(this.triggerExecutor, task, ex); } } @Override public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { try { - return this.scheduledExecutor.scheduleAtFixedRate(scheduledTask(task), + return this.triggerExecutor.scheduleAtFixedRate(scheduledTask(task), 0, NANO.convert(period), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(this.triggerExecutor, task, ex); } } @@ -240,11 +319,11 @@ public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTim Duration initialDelay = Duration.between(this.clock.instant(), startTime); try { // Blocking task on scheduler thread for fixed delay semantics - return this.scheduledExecutor.scheduleWithFixedDelay(task, + return this.fixedDelayExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task), NANO.convert(initialDelay), NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(this.fixedDelayExecutor, task, ex); } } @@ -252,45 +331,54 @@ public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTim public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { try { // Blocking task on scheduler thread for fixed delay semantics - return this.scheduledExecutor.scheduleWithFixedDelay(task, + return this.fixedDelayExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task), 0, NANO.convert(delay), NANO); } catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.scheduledExecutor, task, ex); + throw new TaskRejectedException(this.fixedDelayExecutor, task, ex); } } @Override public void start() { - this.lifecycleDelegate.start(); + this.triggerLifecycle.start(); + this.fixedDelayLifecycle.start(); } @Override public void stop() { - this.lifecycleDelegate.stop(); + this.triggerLifecycle.stop(); + this.fixedDelayLifecycle.stop(); } @Override public void stop(Runnable callback) { - this.lifecycleDelegate.stop(callback); + this.triggerLifecycle.stop(); // no callback necessary since it's just triggers with hand-offs + this.fixedDelayLifecycle.stop(callback); // callback for currently executing fixed-delay tasks } @Override public boolean isRunning() { - return this.lifecycleDelegate.isRunning(); + return this.triggerLifecycle.isRunning(); } @Override public void onApplicationEvent(ContextClosedEvent event) { if (event.getApplicationContext() == this.applicationContext) { - this.scheduledExecutor.shutdown(); + this.triggerExecutor.shutdown(); + this.fixedDelayExecutor.shutdown(); } } @Override public void close() { - for (Runnable remainingTask : this.scheduledExecutor.shutdownNow()) { + for (Runnable remainingTask : this.triggerExecutor.shutdownNow()) { + if (remainingTask instanceof Future future) { + future.cancel(true); + } + } + for (Runnable remainingTask : this.fixedDelayExecutor.shutdownNow()) { if (remainingTask instanceof Future future) { future.cancel(true); } 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 5486c2db6110..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 @@ -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,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} @@ -71,16 +72,17 @@ public class ThreadPoolExecutorFactoryBean extends ExecutorConfigurationSupport private int keepAliveSeconds = 60; + private int queueCapacity = Integer.MAX_VALUE; + private boolean allowCoreThreadTimeOut = false; private boolean prestartAllCoreThreads = false; - private int queueCapacity = Integer.MAX_VALUE; + private boolean strictEarlyShutdown = false; private boolean exposeUnconfigurableExecutor = false; - @Nullable - private ExecutorService exposedExecutor; + private @Nullable ExecutorService exposedExecutor; /** @@ -107,6 +109,18 @@ public void setKeepAliveSeconds(int keepAliveSeconds) { this.keepAliveSeconds = keepAliveSeconds; } + /** + * Set the capacity for the ThreadPoolExecutor's BlockingQueue. + * Default is {@code Integer.MAX_VALUE}. + *

    Any positive value will lead to a LinkedBlockingQueue instance; + * any other value will lead to a SynchronousQueue instance. + * @see java.util.concurrent.LinkedBlockingQueue + * @see java.util.concurrent.SynchronousQueue + */ + public void setQueueCapacity(int queueCapacity) { + this.queueCapacity = queueCapacity; + } + /** * Specify whether to allow core threads to time out. This enables dynamic * growing and shrinking even in combination with a non-zero queue (since @@ -129,15 +143,15 @@ public void setPrestartAllCoreThreads(boolean prestartAllCoreThreads) { } /** - * Set the capacity for the ThreadPoolExecutor's BlockingQueue. - * Default is {@code Integer.MAX_VALUE}. - *

    Any positive value will lead to a LinkedBlockingQueue instance; - * any other value will lead to a SynchronousQueue instance. - * @see java.util.concurrent.LinkedBlockingQueue - * @see java.util.concurrent.SynchronousQueue + * Specify whether to initiate an early shutdown signal on context close, + * disposing all idle threads and rejecting further task submissions. + *

    Default is "false". + * See {@link ThreadPoolTaskExecutor#setStrictEarlyShutdown} for details. + * @since 6.1.4 + * @see #initiateShutdown() */ - public void setQueueCapacity(int queueCapacity) { - this.queueCapacity = queueCapacity; + public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) { + this.strictEarlyShutdown = defaultEarlyShutdown; } /** @@ -222,10 +236,16 @@ protected BlockingQueue createQueue(int queueCapacity) { } } + @Override + protected void initiateEarlyShutdown() { + if (this.strictEarlyShutdown) { + super.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 1a529f6cca94..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 @@ -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. @@ -30,21 +30,20 @@ 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} * in bean style (through its "corePoolSize", "maxPoolSize", "keepAliveSeconds", "queueCapacity" * properties) and exposing it as a Spring {@link org.springframework.core.task.TaskExecutor}. - * This class is also well suited for management and monitoring (e.g. through JMX), + * This class is also well suited for management and monitoring (for example, through JMX), * providing several useful attributes: "corePoolSize", "maxPoolSize", "keepAliveSeconds" * (all supporting updates at runtime); "poolSize", "activeCount" (for introspection only). * @@ -82,7 +81,7 @@ */ @SuppressWarnings({"serial", "deprecation"}) public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport - implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { + implements AsyncTaskExecutor, SchedulingTaskExecutor { private final Object poolSizeMonitor = new Object(); @@ -98,11 +97,11 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport private boolean prestartAllCoreThreads = false; - @Nullable - private TaskDecorator taskDecorator; + private boolean strictEarlyShutdown = false; + + private @Nullable TaskDecorator taskDecorator; - @Nullable - private ThreadPoolExecutor threadPoolExecutor; + private @Nullable ThreadPoolExecutor threadPoolExecutor; // Runnable decorator to user-level FutureTask, if different private final Map decoratedTaskMap = @@ -212,7 +211,7 @@ public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { /** * Specify whether to start all core threads, causing them to idly wait for work. - *

    Default is "false". + *

    Default is "false", starting threads and adding them to the pool on demand. * @since 5.3.14 * @see java.util.concurrent.ThreadPoolExecutor#prestartAllCoreThreads */ @@ -220,6 +219,30 @@ public void setPrestartAllCoreThreads(boolean prestartAllCoreThreads) { this.prestartAllCoreThreads = prestartAllCoreThreads; } + /** + * Specify whether to initiate an early shutdown signal on context close, + * disposing all idle threads and rejecting further task submissions. + *

    By default, existing tasks will be allowed to complete within the + * coordinated lifecycle stop phase in any case. This setting just controls + * whether an explicit {@link ThreadPoolExecutor#shutdown()} call will be + * triggered on context close, rejecting task submissions after that point. + *

    As of 6.1.4, the default is "false", leniently allowing for late tasks + * to arrive after context close, still participating in the lifecycle stop + * phase. Note that this differs from {@link #setAcceptTasksAfterContextClose} + * which completely bypasses the coordinated lifecycle stop phase, with no + * explicit waiting for the completion of existing tasks at all. + *

    Switch this to "true" for a strict early shutdown signal analogous to + * the 6.1-established default behavior of {@link ThreadPoolTaskScheduler}. + * Note that the related flags {@link #setAcceptTasksAfterContextClose} and + * {@link #setWaitForTasksToCompleteOnShutdown} will override this setting, + * leading to a late shutdown without a coordinated lifecycle stop phase. + * @since 6.1.4 + * @see #initiateShutdown() + */ + public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) { + this.strictEarlyShutdown = defaultEarlyShutdown; + } + /** * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} * about to be executed. @@ -292,7 +315,7 @@ protected void afterExecute(Runnable task, Throwable ex) { /** * Create the BlockingQueue to use for the ThreadPoolExecutor. *

    A LinkedBlockingQueue instance will be created for a positive - * capacity value; a SynchronousQueue else. + * capacity value; a SynchronousQueue otherwise. * @param queueCapacity the specified queue capacity * @return the BlockingQueue instance * @see java.util.concurrent.LinkedBlockingQueue @@ -388,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); @@ -424,4 +421,11 @@ protected void cancelRemainingTask(Runnable task) { } } + @Override + protected void initiateEarlyShutdown() { + if (this.strictEarlyShutdown) { + super.initiateEarlyShutdown(); + } + } + } 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 9ab42bae99eb..b0d754cc670c 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-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,36 +19,45 @@ 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; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.RunnableScheduledFuture; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; 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 * a native {@link java.util.concurrent.ScheduledThreadPoolExecutor} and providing - * all applicable configuration options for it. + * all applicable configuration options for it. The default number of scheduler + * threads is 1; a higher number can be configured through {@link #setPoolSize}. + * + *

    This is Spring's traditional scheduler variant, staying as close as possible to + * {@link java.util.concurrent.ScheduledExecutorService} semantics. Task execution happens + * on the scheduler thread(s) rather than on separate execution threads. As a consequence, + * a {@link ScheduledFuture} handle (for example, from {@link #schedule(Runnable, Instant)}) + * represents the actual completion of the provided task (or series of repeated tasks). * * @author Juergen Hoeller * @author Mark Fisher @@ -59,10 +68,12 @@ * @see #setExecuteExistingDelayedTasksAfterShutdownPolicy * @see #setThreadFactory * @see #setErrorHandler + * @see ThreadPoolTaskExecutor + * @see SimpleAsyncTaskScheduler */ @SuppressWarnings({"serial", "deprecation"}) public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport - implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler { + implements AsyncTaskExecutor, SchedulingTaskExecutor, TaskScheduler { private static final TimeUnit NANO = TimeUnit.NANOSECONDS; @@ -75,17 +86,13 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport private volatile boolean executeExistingDelayedTasksAfterShutdownPolicy = true; - @Nullable - private volatile ErrorHandler errorHandler; + private @Nullable TaskDecorator taskDecorator; - private Clock clock = Clock.systemDefaultZone(); + private volatile @Nullable ErrorHandler errorHandler; - @Nullable - private ScheduledExecutorService scheduledExecutor; + private Clock clock = Clock.systemDefaultZone(); - // Underlying ScheduledFutureTask to user-level ListenableFuture handle, if any - private final Map> listenableFutureMap = - new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + private @Nullable ScheduledExecutorService scheduledExecutor; /** @@ -145,6 +152,20 @@ public void setExecuteExistingDelayedTasksAfterShutdownPolicy(boolean flag) { this.executeExistingDelayedTasksAfterShutdownPolicy = flag; } + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + *

    Note that such a decorator is not being applied to the user-supplied + * {@code Runnable}/{@code Callable} but rather to the scheduled execution + * callback (a wrapper around the user-supplied task). + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + * @since 6.2 + */ + public void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; + } + /** * Set a custom {@link ErrorHandler} strategy. */ @@ -159,6 +180,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -212,6 +234,14 @@ protected void beforeExecute(Thread thread, Runnable task) { protected void afterExecute(Runnable task, Throwable ex) { ThreadPoolTaskScheduler.this.afterExecute(task, ex); } + @Override + protected RunnableScheduledFuture decorateTask(Runnable runnable, RunnableScheduledFuture task) { + return decorateTaskIfNecessary(task); + } + @Override + protected RunnableScheduledFuture decorateTask(Callable callable, RunnableScheduledFuture task) { + return decorateTaskIfNecessary(task); + } }; } @@ -310,67 +340,18 @@ public Future submit(Runnable task) { public Future submit(Callable task) { ExecutorService executor = getScheduledExecutor(); try { - Callable taskToUse = task; - ErrorHandler errorHandler = this.errorHandler; - if (errorHandler != null) { - taskToUse = new DelegatingErrorHandlingCallable<>(task, errorHandler); - } - return executor.submit(taskToUse); + return executor.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); } catch (RejectedExecutionException ex) { throw new TaskRejectedException(executor, task, ex); } } - @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; @@ -447,32 +428,70 @@ public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) } + private RunnableScheduledFuture decorateTaskIfNecessary(RunnableScheduledFuture future) { + return (this.taskDecorator != null ? new DelegatingRunnableScheduledFuture<>(future, this.taskDecorator) : + future); + } + private Runnable errorHandlingTask(Runnable task, boolean isRepeatingTask) { return TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, isRepeatingTask); } - private static class DelegatingErrorHandlingCallable implements Callable { + private static class DelegatingRunnableScheduledFuture implements RunnableScheduledFuture { - private final Callable delegate; + private final RunnableScheduledFuture future; - private final ErrorHandler errorHandler; + private final Runnable decoratedRunnable; - public DelegatingErrorHandlingCallable(Callable delegate, ErrorHandler errorHandler) { - this.delegate = delegate; - this.errorHandler = errorHandler; + public DelegatingRunnableScheduledFuture(RunnableScheduledFuture future, TaskDecorator taskDecorator) { + this.future = future; + this.decoratedRunnable = taskDecorator.decorate(this.future); } @Override - @Nullable - public V call() throws Exception { - try { - return this.delegate.call(); - } - catch (Throwable ex) { - this.errorHandler.handleError(ex); - return null; - } + public void run() { + this.decoratedRunnable.run(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return this.future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return this.future.isCancelled(); + } + + @Override + public boolean isDone() { + return this.future.isDone(); + } + + @Override + public V get() throws InterruptedException, ExecutionException { + return this.future.get(); + } + + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return this.future.get(timeout, unit); + } + + @Override + public boolean isPeriodic() { + return this.future.isPeriodic(); + } + + @Override + public long getDelay(TimeUnit unit) { + return this.future.getDelay(unit); + } + + @Override + public int compareTo(Delayed o) { + return this.future.compareTo(o); } } 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/ExecutorBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java index 7c53292786ab..3516eff33b47 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -18,6 +18,7 @@ import org.w3c.dom.Element; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; @@ -53,6 +54,7 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit if (StringUtils.hasText(poolSize)) { builder.addPropertyValue("poolSize", poolSize); } + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); } private void configureRejectionPolicy(Element element, BeanDefinitionBuilder builder) { @@ -61,22 +63,13 @@ private void configureRejectionPolicy(Element element, BeanDefinitionBuilder bui return; } String prefix = "java.util.concurrent.ThreadPoolExecutor."; - String policyClassName; - if (rejectionPolicy.equals("ABORT")) { - policyClassName = prefix + "AbortPolicy"; - } - else if (rejectionPolicy.equals("CALLER_RUNS")) { - policyClassName = prefix + "CallerRunsPolicy"; - } - else if (rejectionPolicy.equals("DISCARD")) { - policyClassName = prefix + "DiscardPolicy"; - } - else if (rejectionPolicy.equals("DISCARD_OLDEST")) { - policyClassName = prefix + "DiscardOldestPolicy"; - } - else { - policyClassName = rejectionPolicy; - } + String policyClassName = switch (rejectionPolicy) { + case "ABORT" -> prefix + "AbortPolicy"; + case "CALLER_RUNS" -> prefix + "CallerRunsPolicy"; + case "DISCARD" -> prefix + "DiscardPolicy"; + case "DISCARD_OLDEST" -> prefix + "DiscardOldestPolicy"; + default -> rejectionPolicy; + }; builder.addPropertyValue("rejectedExecutionHandler", new RootBeanDefinition(policyClassName)); } 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 799f87464b4a..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 @@ -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,15 +16,18 @@ package org.springframework.scheduling.config; +import java.time.Instant; 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, * used as a return value for scheduling methods. * * @author Juergen Hoeller + * @author Brian Clozel * @since 4.3 * @see ScheduledTaskRegistrar#scheduleCronTask(CronTask) * @see ScheduledTaskRegistrar#scheduleFixedRateTask(FixedRateTask) @@ -35,8 +38,7 @@ public final class ScheduledTask { private final Task task; - @Nullable - volatile ScheduledFuture future; + volatile @Nullable ScheduledFuture future; ScheduledTask(Task task) { @@ -76,6 +78,22 @@ public void cancel(boolean mayInterruptIfRunning) { } } + /** + * Return the next scheduled execution of the task, or {@code null} + * if the task has been cancelled or no new execution is scheduled. + * @since 6.2 + */ + public @Nullable Instant nextExecution() { + ScheduledFuture future = this.future; + if (future != null && !future.isCancelled()) { + long delay = future.getDelay(TimeUnit.MILLISECONDS); + if (delay > 0) { + return Instant.now().plusMillis(delay); + } + } + return null; + } + @Override public String toString() { return this.task.toString(); 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 02cadb58f5ca..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 @@ -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. @@ -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; } @@ -295,8 +285,8 @@ public void addTriggerTask(TriggerTask task) { /** * Add a {@link Runnable} task to be triggered per the given cron {@code expression}. - *

    As of Spring Framework 5.2, this method will not register the task if the - * {@code expression} is equal to {@link #CRON_DISABLED}. + *

    This method will not register the task if the {@code expression} is + * equal to {@link #CRON_DISABLED}. */ public void addCronTask(Runnable task, String expression) { if (!CRON_DISABLED.equals(expression)) { @@ -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/SchedulerBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java index a9429e4901aa..4eb9e1c1431e 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import org.w3c.dom.Element; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.util.StringUtils; @@ -41,6 +42,7 @@ protected void doParse(Element element, BeanDefinitionBuilder builder) { if (StringUtils.hasText(poolSize)) { builder.addPropertyValue("poolSize", poolSize); } + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/Task.java b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java index 3d9a6e79a775..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 @@ -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,6 +16,11 @@ package org.springframework.scheduling.config; +import java.time.Instant; + +import org.jspecify.annotations.Nullable; + +import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.util.Assert; /** @@ -24,12 +29,15 @@ * * @author Chris Beams * @author Juergen Hoeller + * @author Brian Clozel * @since 3.2 */ public class Task { private final Runnable runnable; + private TaskExecutionOutcome lastExecutionOutcome; + /** * Create a new {@code Task}. @@ -37,7 +45,8 @@ public class Task { */ public Task(Runnable runnable) { Assert.notNull(runnable, "Runnable must not be null"); - this.runnable = runnable; + this.runnable = new OutcomeTrackingRunnable(runnable); + this.lastExecutionOutcome = TaskExecutionOutcome.create(); } @@ -48,10 +57,61 @@ public Runnable getRunnable() { return this.runnable; } + /** + * Return the outcome of the last task execution. + * @since 6.2 + */ + public TaskExecutionOutcome getLastExecutionOutcome() { + return this.lastExecutionOutcome; + } @Override public String toString() { return this.runnable.toString(); } + + private class OutcomeTrackingRunnable implements SchedulingAwareRunnable { + + private final Runnable runnable; + + public OutcomeTrackingRunnable(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void run() { + try { + Task.this.lastExecutionOutcome = Task.this.lastExecutionOutcome.start(Instant.now()); + this.runnable.run(); + Task.this.lastExecutionOutcome = Task.this.lastExecutionOutcome.success(); + } + catch (Throwable exc) { + Task.this.lastExecutionOutcome = Task.this.lastExecutionOutcome.failure(exc); + throw exc; + } + } + + @Override + public boolean isLongLived() { + if (this.runnable instanceof SchedulingAwareRunnable sar) { + return sar.isLongLived(); + } + return SchedulingAwareRunnable.super.isLongLived(); + } + + @Override + public @Nullable String getQualifier() { + if (this.runnable instanceof SchedulingAwareRunnable sar) { + return sar.getQualifier(); + } + return SchedulingAwareRunnable.super.getQualifier(); + } + + @Override + public String toString() { + return this.runnable.toString(); + } + } + } 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 new file mode 100644 index 000000000000..b049296dc08f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutionOutcome.java @@ -0,0 +1,83 @@ +/* + * 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.config; + +import java.time.Instant; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * Outcome of a {@link Task} execution. + * + * @author Brian Clozel + * @since 6.2 + * @param executionTime the instant when the task execution started, or + * {@code null} if the task has not started + * @param status the {@link Status} of the execution outcome + * @param throwable the exception thrown from the task execution, if any + */ +public record TaskExecutionOutcome(@Nullable Instant executionTime, Status status, @Nullable Throwable throwable) { + + TaskExecutionOutcome start(Instant executionTime) { + return new TaskExecutionOutcome(executionTime, Status.STARTED, null); + } + + TaskExecutionOutcome success() { + Assert.state(this.executionTime != null, "Task has not been started yet"); + return new TaskExecutionOutcome(this.executionTime, Status.SUCCESS, null); + } + + TaskExecutionOutcome failure(Throwable throwable) { + Assert.state(this.executionTime != null, "Task has not been started yet"); + return new TaskExecutionOutcome(this.executionTime, Status.ERROR, throwable); + } + + static TaskExecutionOutcome create() { + return new TaskExecutionOutcome(null, Status.NONE, null); + } + + + /** + * Status of the task execution outcome. + */ + public enum Status { + + /** + * The task has not been executed so far. + */ + NONE, + + /** + * The task execution has been started and is ongoing. + */ + STARTED, + + /** + * The task execution finished successfully. + */ + SUCCESS, + + /** + * The task execution finished with an error. + */ + ERROR + + } + +} 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 abb3f7315e3e..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) { @@ -137,15 +132,14 @@ private void determinePoolSizeRange(ThreadPoolTaskExecutor executor) { } catch (NumberFormatException ex) { throw new IllegalArgumentException("Invalid pool-size value [" + this.poolSize + "]: only single " + - "maximum integer (e.g. \"5\") and minimum-maximum range (e.g. \"3-5\") are supported", ex); + "maximum integer (for example, \"5\") and minimum-maximum range (for example, \"3-5\") are supported", ex); } } } @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 480bbca6fe7f..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,7 +102,7 @@ public void setBeanFactory(@Nullable BeanFactory beanFactory) { @Override - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { return determineTargetScheduler(task).schedule(task, trigger); } @@ -149,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 d3394426347f..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 @@ -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,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; @@ -29,16 +30,14 @@ * Created using the {@code parse*} methods. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.3 */ final class BitsCronField extends CronField { - private static final long MASK = 0xFFFFFFFFFFFFFFFFL; - - - @Nullable - private static BitsCronField zeroNanos = null; + public static final BitsCronField ZERO_NANOS = forZeroNanos(); + private static final long MASK = 0xFFFFFFFFFFFFFFFFL; // we store at most 60 bits, for seconds and minutes, so a 64-bit long suffices private long bits; @@ -48,16 +47,14 @@ private BitsCronField(Type type) { super(type); } + /** * Return a {@code BitsCronField} enabled for 0 nanoseconds. */ - public static BitsCronField zeroNanos() { - if (zeroNanos == null) { - BitsCronField field = new BitsCronField(Type.NANO); - field.setBit(0); - zeroNanos = field; - } - return zeroNanos; + private static BitsCronField forZeroNanos() { + BitsCronField field = new BitsCronField(Type.NANO); + field.setBit(0); + return field; } /** @@ -108,7 +105,6 @@ public static BitsCronField parseDaysOfWeek(String value) { return result; } - private static BitsCronField parseDate(String value, BitsCronField.Type type) { if (value.equals("?")) { value = "*"; @@ -174,9 +170,9 @@ 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) { @@ -217,7 +213,6 @@ private int nextSetBit(int fromIndex) { else { return -1; } - } private void setBits(ValueRange range) { @@ -250,20 +245,16 @@ private void clearBit(int index) { this.bits &= ~(1L << index); } + @Override - public int hashCode() { - return Long.hashCode(this.bits); + public boolean equals(Object other) { + return (this == other || (other instanceof BitsCronField that && + type() == that.type() && this.bits == that.bits)); } @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (!(o instanceof BitsCronField other)) { - return false; - } - return type() == other.type() && this.bits == other.bits; + public int hashCode() { + return Long.hashCode(this.bits); } @Override 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 0e6008de6253..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 @@ -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,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; @@ -29,9 +30,14 @@ * crontab expression * that can calculate the next time it matches. * - *

    {@code CronExpression} instances are created through - * {@link #parse(String)}; the next match is determined with - * {@link #next(Temporal)}. + *

    {@code CronExpression} instances are created through {@link #parse(String)}; + * the next match is determined with {@link #next(Temporal)}. + * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Arjen Poutsma * @since 5.3 @@ -137,11 +143,11 @@ private CronExpression(CronField seconds, CronField minutes, CronField hours, * *

    Example expressions: *

      - *
    • {@code "0 0 * * * *"} = the top of every hour of every day.
    • - *
    • "*/10 * * * * *" = every ten seconds.
    • - *
    • {@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.
    • - *
    • {@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.
    • - *
    • {@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
    • + *
    • {@code "0 0 * * * *"} = the top of every hour of every day
    • + *
    • "*/10 * * * * *" = every ten seconds
    • + *
    • {@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day
    • + *
    • {@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day
    • + *
    • {@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day
    • *
    • {@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays
    • *
    • {@code "0 0 0 25 12 ?"} = every Christmas Day at midnight
    • *
    • {@code "0 0 0 L * *"} = last day of the month at midnight
    • @@ -154,13 +160,13 @@ private CronExpression(CronField seconds, CronField minutes, CronField hours, *
    • {@code "0 0 0 ? * MON#1"} = the first Monday in the month at midnight
    • *
    * - *

    The following macros are also supported: + *

    The following macros are also supported. *

      - *
    • {@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"},
    • - *
    • {@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"},
    • - *
    • {@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"},
    • - *
    • {@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"},
    • - *
    • {@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}.
    • + *
    • {@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"}
    • + *
    • {@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"}
    • + *
    • {@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"}
    • + *
    • {@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"}
    • + *
    • {@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}
    • *
    * @param expression the expression string to parse * @return the parsed {@code CronExpression} object @@ -168,7 +174,7 @@ private CronExpression(CronField seconds, CronField minutes, CronField hours, * the cron format */ public static CronExpression parse(String expression) { - Assert.hasLength(expression, "Expression string must not be empty"); + Assert.hasLength(expression, "Expression must not be empty"); expression = resolveMacros(expression); @@ -231,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)) { @@ -249,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 99d940613e5c..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 @@ -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. @@ -21,25 +21,34 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.Temporal; import java.time.temporal.ValueRange; +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; /** * Single field in a cron pattern. Created using the {@code parse*} methods, - * main and only entry point is {@link #nextOrSame(Temporal)}. + * the main and only entry point is {@link #nextOrSame(Temporal)}. + * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Arjen Poutsma * @since 5.3 */ abstract class CronField { - private static final String[] MONTHS = new String[]{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", - "OCT", "NOV", "DEC"}; + private static final String[] MONTHS = new String[] + {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; - private static final String[] DAYS = new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; + private static final String[] DAYS = new String[] + {"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; private final Type type; @@ -48,11 +57,12 @@ protected CronField(Type type) { this.type = type; } + /** * Return a {@code CronField} enabled for 0 nanoseconds. */ public static CronField zeroNanos() { - return BitsCronField.zeroNanos(); + return BitsCronField.ZERO_NANOS; } /** @@ -135,7 +145,7 @@ private static CronField parseList(String value, Type type, BiFunction> T nextOrSame(T temporal); + public abstract > @Nullable T nextOrSame(T temporal); protected Type type() { @@ -169,6 +178,7 @@ protected static > T cast(Temporal te * day-of-month, month, day-of-week. */ protected enum Type { + NANO(ChronoField.NANO_OF_SECOND, ChronoUnit.SECONDS), SECOND(ChronoField.SECOND_OF_MINUTE, ChronoUnit.MINUTES, ChronoField.NANO_OF_SECOND), MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoUnit.HOURS, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), @@ -177,21 +187,18 @@ protected enum Type { MONTH(ChronoField.MONTH_OF_YEAR, ChronoUnit.YEARS, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoUnit.WEEKS, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); - private final ChronoField field; private final ChronoUnit higherOrder; private final ChronoField[] lowerOrders; - Type(ChronoField field, ChronoUnit higherOrder, ChronoField... lowerOrders) { this.field = field; this.higherOrder = higherOrder; this.lowerOrders = lowerOrders; } - /** * Return the value of this type for the given temporal. * @return the value of this 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 7bcb8ed2fa74..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 @@ -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,14 +21,21 @@ 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; /** - * {@link Trigger} implementation for cron expressions. - * Wraps a {@link CronExpression}. + * {@link Trigger} implementation for cron expressions. Wraps a + * {@link CronExpression} which parses according to common crontab conventions. + * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Juergen Hoeller * @author Arjen Poutsma @@ -39,30 +46,44 @@ public class CronTrigger implements Trigger { private final CronExpression expression; - private final ZoneId zoneId; + private final @Nullable ZoneId zoneId; /** * Build a {@code CronTrigger} from the pattern provided in the default time zone. + *

    This is equivalent to the {@link CronTrigger#forLenientExecution} factory + * method. Original trigger firings may be skipped if the previous task is still + * running; if this is not desirable, consider {@link CronTrigger#forFixedExecution}. * @param expression a space-separated list of time fields, following cron * expression conventions + * @see CronTrigger#forLenientExecution + * @see CronTrigger#forFixedExecution */ public CronTrigger(String expression) { - this(expression, ZoneId.systemDefault()); + this.expression = CronExpression.parse(expression); + this.zoneId = null; } /** - * Build a {@code CronTrigger} from the pattern provided in the given time zone. + * Build a {@code CronTrigger} from the pattern provided in the given time zone, + * with the same lenient execution as {@link CronTrigger#CronTrigger(String)}. + *

    Note that such explicit time zone customization is usually not necessary, + * using {@link org.springframework.scheduling.TaskScheduler#getClock()} instead. * @param expression a space-separated list of time fields, following cron * expression conventions * @param timeZone a time zone in which the trigger times will be generated */ public CronTrigger(String expression, TimeZone timeZone) { - this(expression, timeZone.toZoneId()); + this.expression = CronExpression.parse(expression); + Assert.notNull(timeZone, "TimeZone must not be null"); + this.zoneId = timeZone.toZoneId(); } /** - * Build a {@code CronTrigger} from the pattern provided in the given time zone. + * Build a {@code CronTrigger} from the pattern provided in the given time zone, + * with the same lenient execution as {@link CronTrigger#CronTrigger(String)}. + *

    Note that such explicit time zone customization is usually not necessary, + * using {@link org.springframework.scheduling.TaskScheduler#getClock()} instead. * @param expression a space-separated list of time fields, following cron * expression conventions * @param zoneId a time zone in which the trigger times will be generated @@ -70,10 +91,8 @@ public CronTrigger(String expression, TimeZone timeZone) { * @see CronExpression#parse(String) */ public CronTrigger(String expression, ZoneId zoneId) { - Assert.hasLength(expression, "Expression must not be empty"); - Assert.notNull(zoneId, "ZoneId must not be null"); - this.expression = CronExpression.parse(expression); + Assert.notNull(zoneId, "ZoneId must not be null"); this.zoneId = zoneId; } @@ -93,23 +112,33 @@ public String getExpression() { * previous execution; therefore, overlapping executions won't occur. */ @Override - public Instant nextExecution(TriggerContext triggerContext) { - Instant instant = triggerContext.lastCompletion(); - if (instant != null) { + 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); + ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp); + return (nextTimestamp != null ? nextTimestamp.toInstant() : null); + } + + Instant determineLatestTimestamp(TriggerContext triggerContext) { + Instant timestamp = triggerContext.lastCompletion(); + if (timestamp != null) { Instant scheduled = triggerContext.lastScheduledExecution(); - if (scheduled != null && instant.isBefore(scheduled)) { + if (scheduled != null && timestamp.isBefore(scheduled)) { // Previous task apparently executed too early... // Let's simply use the last calculated execution time then, // in order to prevent accidental re-fires in the same second. - instant = scheduled; + timestamp = scheduled; } } else { - instant = triggerContext.getClock().instant(); + timestamp = determineInitialTimestamp(triggerContext); } - ZonedDateTime dateTime = ZonedDateTime.ofInstant(instant, this.zoneId); - ZonedDateTime next = this.expression.next(dateTime); - return (next != null ? next.toInstant() : null); + return timestamp; + } + + Instant determineInitialTimestamp(TriggerContext triggerContext) { + return triggerContext.getClock().instant(); } @@ -129,4 +158,99 @@ public String toString() { return this.expression.toString(); } + + /** + * Create a {@link CronTrigger} for lenient execution, to be rescheduled + * after every task based on the completion time. + *

    This variant does not make up for missed trigger firings if the + * associated task has taken too long. As a consequence, original trigger + * firings may be skipped if the previous task is still running. + *

    This is equivalent to the regular {@link CronTrigger} constructor. + * Note that lenient execution is scheduler-dependent: it may skip trigger + * firings with long-running tasks on a thread pool while executing at + * {@link #forFixedExecution}-like precision with new threads per task. + * @param expression a space-separated list of time fields, following cron + * expression conventions + * @since 6.1.3 + * @see #resumeLenientExecution + */ + public static CronTrigger forLenientExecution(String expression) { + return new CronTrigger(expression); + } + + /** + * Create a {@link CronTrigger} for lenient execution, to be rescheduled + * after every task based on the completion time. + *

    This variant does not make up for missed trigger firings if the + * associated task has taken too long. As a consequence, original trigger + * firings may be skipped if the previous task is still running. + * @param expression a space-separated list of time fields, following cron + * expression conventions + * @param resumptionTimestamp the timestamp to resume from (the last-known + * completion timestamp), with the new trigger calculated from there and + * possibly immediately firing (but only once, every subsequent calculation + * will start from the completion time of that first resumed trigger) + * @since 6.1.3 + * @see #forLenientExecution + */ + public static CronTrigger resumeLenientExecution(String expression, Instant resumptionTimestamp) { + return new CronTrigger(expression) { + @Override + Instant determineInitialTimestamp(TriggerContext triggerContext) { + return resumptionTimestamp; + } + }; + } + + /** + * Create a {@link CronTrigger} for fixed execution, to be rescheduled + * after every task based on the last scheduled time. + *

    This variant makes up for missed trigger firings if the associated task + * has taken too long, scheduling a task for every original trigger firing. + * Such follow-up tasks may execute late but will never be skipped. + *

    Immediate versus late execution in case of long-running tasks may + * be scheduler-dependent but the guarantee to never skip a task is portable. + * @param expression a space-separated list of time fields, following cron + * expression conventions + * @since 6.1.3 + * @see #resumeFixedExecution + */ + public static CronTrigger forFixedExecution(String expression) { + return new CronTrigger(expression) { + @Override + protected Instant determineLatestTimestamp(TriggerContext triggerContext) { + Instant scheduled = triggerContext.lastScheduledExecution(); + return (scheduled != null ? scheduled : super.determineInitialTimestamp(triggerContext)); + } + }; + } + + /** + * Create a {@link CronTrigger} for fixed execution, to be rescheduled + * after every task based on the last scheduled time. + *

    This variant makes up for missed trigger firings if the associated task + * has taken too long, scheduling a task for every original trigger firing. + * Such follow-up tasks may execute late but will never be skipped. + * @param expression a space-separated list of time fields, following cron + * expression conventions + * @param resumptionTimestamp the timestamp to resume from (the last-known + * scheduled timestamp), with every trigger in-between immediately firing + * to make up for every execution that would have happened in the meantime + * @since 6.1.3 + * @see #forFixedExecution + */ + public static CronTrigger resumeFixedExecution(String expression, Instant resumptionTimestamp) { + return new CronTrigger(expression) { + @Override + protected Instant determineLatestTimestamp(TriggerContext triggerContext) { + Instant scheduled = triggerContext.lastScheduledExecution(); + return (scheduled != null ? scheduled : super.determineLatestTimestamp(triggerContext)); + } + @Override + Instant determineInitialTimestamp(TriggerContext triggerContext) { + return resumptionTimestamp; + } + }; + } + } 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 46c0385757ff..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 @@ -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,10 +19,9 @@ import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; +import org.springframework.scheduling.support.ScheduledTaskObservationDocumentation.LowCardinalityKeyNames; import org.springframework.util.StringUtils; -import static org.springframework.scheduling.support.ScheduledTaskObservationDocumentation.LowCardinalityKeyNames; - /** * Default implementation for {@link ScheduledTaskObservationConvention}. * @author Brian Clozel @@ -40,6 +39,8 @@ public class DefaultScheduledTaskObservationConvention implements ScheduledTaskO private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "UNKNOWN"); + private static final KeyValue CODE_NAMESPACE_ANONYMOUS = KeyValue.of(LowCardinalityKeyNames.CODE_NAMESPACE, "ANONYMOUS"); + @Override public String getName() { return DEFAULT_NAME; @@ -47,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 @@ -61,7 +62,10 @@ protected KeyValue codeFunction(ScheduledTaskObservationContext context) { } protected KeyValue codeNamespace(ScheduledTaskObservationContext context) { - return KeyValue.of(LowCardinalityKeyNames.CODE_NAMESPACE, context.getTargetClass().getCanonicalName()); + if (context.getTargetClass().getCanonicalName() != null) { + return KeyValue.of(LowCardinalityKeyNames.CODE_NAMESPACE, context.getTargetClass().getCanonicalName()); + } + return CODE_NAMESPACE_ANONYMOUS; } protected KeyValue exception(ScheduledTaskObservationContext context) { 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 new file mode 100644 index 000000000000..cf5680de944c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/NoOpTaskScheduler.java @@ -0,0 +1,112 @@ +/* + * 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.scheduling.support; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.CancellationException; +import java.util.concurrent.Delayed; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.jspecify.annotations.Nullable; + +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; + +/** + * A basic, no operation {@link TaskScheduler} implementation suitable + * for disabling scheduling, typically used for test setups. + * + *

    Will accept any scheduling request but never actually execute it. + * + * @author Juergen Hoeller + * @since 6.1.3 + */ +public class NoOpTaskScheduler implements TaskScheduler { + + @Override + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { + Instant nextExecution = trigger.nextExecution(new SimpleTriggerContext(getClock())); + return (nextExecution != null ? new NoOpScheduledFuture<>() : null); + } + + @Override + public ScheduledFuture schedule(Runnable task, Instant startTime) { + return new NoOpScheduledFuture<>(); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { + return new NoOpScheduledFuture<>(); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { + return new NoOpScheduledFuture<>(); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { + return new NoOpScheduledFuture<>(); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { + return new NoOpScheduledFuture<>(); + } + + + private static class NoOpScheduledFuture implements ScheduledFuture { + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return true; + } + + @Override + public boolean isCancelled() { + return true; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public V get() { + throw new CancellationException("No-op"); + } + + @Override + public V get(long timeout, TimeUnit unit) { + throw new CancellationException("No-op"); + } + + @Override + public long getDelay(TimeUnit unit) { + return 0; + } + + @Override + public int compareTo(Delayed other) { + return 0; + } + } + +} 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 d1f0a547071e..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 @@ -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,16 +24,22 @@ 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; /** * Extension of {@link CronField} for * Quartz-specific fields. - * - *

    Created using the {@code parse*} methods, uses a {@link TemporalAdjuster} + * Created using the {@code parse*} methods, uses a {@link TemporalAdjuster} * internally. * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. + * * @author Arjen Poutsma * @since 5.3 */ @@ -61,8 +67,9 @@ private QuartzCronField(Type type, Type rollForwardType, TemporalAdjuster adjust this.rollForwardType = rollForwardType; } + /** - * Returns whether the given value is a Quartz day-of-month field. + * Determine whether the given value is a Quartz day-of-month field. */ public static boolean isQuartzDaysOfMonthField(String value) { return value.contains("L") || value.contains("W"); @@ -80,14 +87,14 @@ public static QuartzCronField parseDaysOfMonth(String value) { if (idx != 0) { throw new IllegalArgumentException("Unrecognized characters before 'L' in '" + value + "'"); } - else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" + else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" adjuster = lastWeekdayOfMonth(); } else { - if (value.length() == 1) { // "L" + if (value.length() == 1) { // "L" adjuster = lastDayOfMonth(); } - else { // "L-[0-9]+" + else { // "L-[0-9]+" int offset = Integer.parseInt(value, idx + 1, value.length(), 10); if (offset >= 0) { throw new IllegalArgumentException("Offset '" + offset + " should be < 0 '" + value + "'"); @@ -105,7 +112,7 @@ else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" else if (idx != value.length() - 1) { throw new IllegalArgumentException("Unrecognized characters after 'W' in '" + value + "'"); } - else { // "[0-9]+W" + else { // "[0-9]+W" int dayOfMonth = Integer.parseInt(value, 0, idx, 10); dayOfMonth = Type.DAY_OF_MONTH.checkValidValue(dayOfMonth); TemporalAdjuster adjuster = weekdayNearestTo(dayOfMonth); @@ -116,7 +123,7 @@ else if (idx != value.length() - 1) { } /** - * Returns whether the given value is a Quartz day-of-week field. + * Determine whether the given value is a Quartz day-of-week field. */ public static boolean isQuartzDaysOfWeekField(String value) { return value.contains("L") || value.contains("#"); @@ -138,7 +145,7 @@ public static QuartzCronField parseDaysOfWeek(String value) { if (idx == 0) { throw new IllegalArgumentException("No day-of-week before 'L' in '" + value + "'"); } - else { // "[0-7]L" + else { // "[0-7]L" DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx)); adjuster = lastInMonth(dayOfWeek); } @@ -160,7 +167,6 @@ else if (idx == value.length() - 1) { throw new IllegalArgumentException("Ordinal '" + ordinal + "' in '" + value + "' must be positive number "); } - TemporalAdjuster adjuster = dayOfWeekInMonth(ordinal, dayOfWeek); return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value); } @@ -170,14 +176,13 @@ else if (idx == value.length() - 1) { private static DayOfWeek parseDayOfWeek(String value) { int dayOfWeek = Integer.parseInt(value); if (dayOfWeek == 0) { - dayOfWeek = 7; // cron is 0 based; java.time 1 based + dayOfWeek = 7; // cron is 0 based; java.time 1 based } try { return DayOfWeek.of(dayOfWeek); } catch (DateTimeException ex) { - String msg = ex.getMessage() + " '" + value + "'"; - throw new IllegalArgumentException(msg, ex); + throw new IllegalArgumentException(ex.getMessage() + " '" + value + "'", ex); } } @@ -216,10 +221,10 @@ private static TemporalAdjuster lastWeekdayOfMonth() { Temporal lastDom = adjuster.adjustInto(temporal); Temporal result; int dow = lastDom.get(ChronoField.DAY_OF_WEEK); - if (dow == 6) { // Saturday + if (dow == 6) { // Saturday result = lastDom.minus(1, ChronoUnit.DAYS); } - else if (dow == 7) { // Sunday + else if (dow == 7) { // Sunday result = lastDom.minus(2, ChronoUnit.DAYS); } else { @@ -256,10 +261,10 @@ private static TemporalAdjuster weekdayNearestTo(int dayOfMonth) { int current = Type.DAY_OF_MONTH.get(temporal); DayOfWeek dayOfWeek = DayOfWeek.from(temporal); - if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday - (dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before - (dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after - (dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd + if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday + (dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before + (dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after + (dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd return temporal; } int count = 0; @@ -313,8 +318,16 @@ private static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek) { private static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) { TemporalAdjuster adjuster = TemporalAdjusters.dayOfWeekInMonth(ordinal, dayOfWeek); return temporal -> { - Temporal result = adjuster.adjustInto(temporal); - return rollbackToMidnight(temporal, result); + // TemporalAdjusters can overflow to a different month + // in this case, attempt the same adjustment with the next/previous month + for (int i = 0; i < 12; i++) { + Temporal result = adjuster.adjustInto(temporal); + if (result.get(ChronoField.MONTH_OF_YEAR) == temporal.get(ChronoField.MONTH_OF_YEAR)) { + return rollbackToMidnight(temporal, result); + } + temporal = result; + } + return null; }; } @@ -332,8 +345,9 @@ private static Temporal rollbackToMidnight(Temporal current, Temporal result) { } } + @Override - public > T nextOrSame(T temporal) { + public > @Nullable T nextOrSame(T temporal) { T result = adjust(temporal); if (result != null) { if (result.compareTo(temporal) < 0) { @@ -348,35 +362,26 @@ 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); } @Override - public int hashCode() { - return this.value.hashCode(); + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof QuartzCronField that && + type() == that.type() && this.value.equals(that.value))); } @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (!(o instanceof QuartzCronField other)) { - return false; - } - return type() == other.type() && - this.value.equals(other.value); + public int hashCode() { + return this.value.hashCode(); } @Override public String toString() { return type() + " '" + this.value + "'"; - } } 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 0f0b5d200e89..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; @@ -59,7 +58,7 @@ public class ScheduledMethodRunnable implements SchedulingAwareRunnable { * @param target the target instance to call the method on * @param method the target method to call * @param qualifier a qualifier associated with this Runnable, - * e.g. for determining a scheduler to run this scheduled method on + * for example, for determining a scheduler to run this scheduled method on * @param observationRegistrySupplier a supplier for the observation registry to use * @since 6.1 */ @@ -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 02a76cece6de..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,12 +93,11 @@ 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; /** - * Determine whether a refresh is required (e.g. through + * Determine whether a refresh is required (for example, through * ScriptSource's {@code isModified()} method). * @param scriptSource the actual ScriptSource to retrieve * the script source text from (never {@code null}) 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 ba2e2c4e906d..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,6 +16,7 @@ package org.springframework.scripting.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; @@ -38,7 +39,7 @@ class ScriptingDefaultsParser implements BeanDefinitionParser { @Override - 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 e181bc00205a..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 @@ -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,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(); @@ -233,8 +231,7 @@ public void setBeanFactory(BeanFactory beanFactory) { // Filter out BeanPostProcessors that are part of the AOP infrastructure, // since those are only meant to apply to beans defined in the original factory. - this.scriptBeanFactory.getBeanPostProcessors().removeIf(beanPostProcessor -> - beanPostProcessor instanceof AopInfrastructureBean); + this.scriptBeanFactory.getBeanPostProcessors().removeIf(AopInfrastructureBean.class::isInstance); } @Override @@ -249,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; @@ -305,7 +301,7 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str } @Override - 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 7dcdaa4a20a1..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; /** @@ -80,7 +78,7 @@ public StandardScriptEvaluator(ScriptEngineManager scriptEngineManager) { /** - * Set the name of the language meant for evaluating the scripts (e.g. "Groovy"). + * Set the name of the language meant for evaluating the scripts (for example, "Groovy"). *

    This is effectively an alias for {@link #setEngineName "engineName"}, * potentially (but not yet) providing common abbreviations for certain languages * beyond what the JSR-223 script engine factory exposes. @@ -91,7 +89,7 @@ public void setLanguage(String language) { } /** - * Set the name of the script engine for evaluating the scripts (e.g. "Groovy"), + * Set the name of the script engine for evaluating the scripts (for example, "Groovy"), * as exposed by the JSR-223 script engine factory. * @since 4.2.2 * @see #setLanguage @@ -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/Controller.java b/spring-context/src/main/java/org/springframework/stereotype/Controller.java index a991c31142ee..871f6d9e6ea2 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Controller.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Controller.java @@ -25,7 +25,7 @@ import org.springframework.core.annotation.AliasFor; /** - * Indicates that an annotated class is a "Controller" (e.g. a web controller). + * Indicates that an annotated class is a "Controller" (for example, a web controller). * *

    This annotation serves as a specialization of {@link Component @Component}, * allowing for implementation classes to be autodetected through classpath scanning. 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 0d2b6afaeff0..4247bed6291e 100644 --- a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java +++ b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.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,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; /** @@ -46,7 +47,7 @@ public ConcurrentModel() { } /** - * Construct a new {@code ModelMap} containing the supplied attribute + * Construct a new {@code ConcurrentModel} containing the supplied attribute * under the supplied name. * @see #addAttribute(String, Object) */ @@ -55,8 +56,8 @@ public ConcurrentModel(String attributeName, Object attributeValue) { } /** - * Construct a new {@code ModelMap} containing the supplied attribute. - * Uses attribute name generation to generate the key for the supplied model + * Construct a new {@code ConcurrentModel} containing the supplied attribute. + *

    Uses attribute name generation to generate the key for the supplied model * object. * @see #addAttribute(Object) */ @@ -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 e5374da4a1d1..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 (e.g. 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 2e858a355c6b..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. - * E.g.: 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 da2a5b2411b7..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, e.g. 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 988880e3b9a8..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 @@ -1,10 +1,8 @@ /** * Generic support for UI layer concepts. - * Provides a generic ModelMap for model holding. + *

    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 3886e0859427..584d9f5162e9 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.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. @@ -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; /** @@ -82,7 +83,7 @@ protected void doSetNestedPath(@Nullable String nestedPath) { nestedPath = ""; } nestedPath = canonicalFieldName(nestedPath); - if (nestedPath.length() > 0 && !nestedPath.endsWith(NESTED_PATH_SEPARATOR)) { + if (!nestedPath.isEmpty() && !nestedPath.endsWith(NESTED_PATH_SEPARATOR)) { nestedPath += NESTED_PATH_SEPARATOR; } this.nestedPath = nestedPath; 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 15ef3ae4667f..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,7 +70,7 @@ public void initConversion(ConversionService conversionService) { * @see #getPropertyAccessor() */ @Override - public PropertyEditorRegistry getPropertyEditorRegistry() { + public @Nullable PropertyEditorRegistry getPropertyEditorRegistry() { return (getTarget() != null ? getPropertyAccessor() : null); } @@ -88,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)); } @@ -99,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); } @@ -109,7 +107,7 @@ protected Object getActualFieldValue(String field) { * @see #getCustomEditor */ @Override - 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); @@ -138,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) { @@ -153,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 987414db36ce..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 @@ -31,7 +32,7 @@ *

    Serves as result holder for a {@link DataBinder}, obtained via * the {@link DataBinder#getBindingResult()} method. BindingResult * implementations can also be used directly, for example to invoke - * a {@link Validator} on it (e.g. as part of a unit test). + * a {@link Validator} on it (for example, as part of a unit test). * * @author Juergen Hoeller * @since 2.0 @@ -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 14df019fb0be..6d3c9fbbe0df 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.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,22 +18,26 @@ import java.beans.PropertyEditor; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; 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.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; @@ -49,6 +53,7 @@ import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; +import org.springframework.core.CollectionFactory; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; @@ -56,7 +61,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; @@ -138,21 +142,21 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { */ protected static final Log logger = LogFactory.getLog(DataBinder.class); - @Nullable - private Object target; + /** Internal constant for constructor binding via "[]". */ + private static final int NO_INDEX = -1; - @Nullable - ResolvableType targetType; + + private @Nullable Object target; + + @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; @@ -164,30 +168,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; /** @@ -217,8 +214,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; } @@ -245,8 +241,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; } @@ -522,7 +517,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); } @@ -531,8 +526,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; } @@ -546,11 +540,10 @@ public String[] getAllowedFields() { * well as direct equality. *

    The default implementation of this method stores disallowed field patterns * in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical} - * form. As of Spring Framework 5.2.21, the default implementation also transforms - * disallowed field patterns to {@linkplain String#toLowerCase() lowercase} to - * support case-insensitive pattern matching in {@link #isAllowed}. Subclasses - * which override this method must therefore take both of these transformations - * into account. + * 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. *

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

    Alternatively, specify a list of allowed field patterns. @@ -561,14 +554,15 @@ 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++) { - fieldPatterns[i] = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]).toLowerCase(); + String field = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]); + fieldPatterns[i] = field.toLowerCase(Locale.ROOT); } this.disallowedFields = fieldPatterns; } @@ -579,8 +573,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; } @@ -597,7 +590,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 [" + @@ -609,8 +602,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; } @@ -631,8 +623,7 @@ public void setNameResolver(NameResolver nameResolver) { * constructor parameters. * @since 6.1 */ - @Nullable - public NameResolver getNameResolver() { + public @Nullable NameResolver getNameResolver() { return this.nameResolver; } @@ -682,7 +673,7 @@ public void setValidator(@Nullable Validator validator) { } } - private void assertValidators(Validator... validators) { + private void assertValidators(@Nullable Validator... validators) { Object target = getTarget(); for (Validator validator : validators) { if (validator != null && (target != null && !validator.supports(target.getClass()))) { @@ -723,8 +714,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); } @@ -767,8 +757,7 @@ public void setConversionService(@Nullable ConversionService conversionService) /** * Return the associated ConversionService, if any. */ - @Nullable - public ConversionService getConversionService() { + public @Nullable ConversionService getConversionService() { return this.conversionService; } @@ -841,36 +830,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); @@ -910,8 +894,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); @@ -929,9 +912,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++) { @@ -946,11 +929,24 @@ private Object createObject(ResolvableType objectType, String nestedPath, ValueR String paramPath = nestedPath + lookupName; Class paramType = paramTypes[i]; + ResolvableType resolvableType = ResolvableType.forMethodParameter(param); + Object value = valueResolver.resolveValue(paramPath, paramType); - if (value == null && shouldCreateObject(param)) { - ResolvableType type = ResolvableType.forMethodParameter(param); - args[i] = createObject(type, paramPath + ".", valueResolver); + if (value == null) { + if (List.class.isAssignableFrom(paramType)) { + value = createList(paramPath, paramType, resolvableType, valueResolver); + } + else if (Map.class.isAssignableFrom(paramType)) { + value = createMap(paramPath, paramType, resolvableType, valueResolver); + } + else if (paramType.isArray()) { + value = createArray(paramPath, paramType, resolvableType, valueResolver); + } + } + + if (value == null && shouldConstructArgument(param) && hasValuesFor(paramPath, valueResolver)) { + args[i] = createObject(resolvableType, paramPath + ".", valueResolver); } else { try { @@ -962,11 +958,9 @@ private Object createObject(ResolvableType objectType, String nestedPath, ValueR } } catch (TypeMismatchException ex) { - ex.initPropertyName(paramPath); args[i] = null; failedParamNames.add(paramPath); - getBindingResult().recordFieldValue(paramPath, paramType, value); - getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult()); + handleTypeMismatchException(ex, paramPath, paramType, value); } } } @@ -1008,17 +1002,172 @@ private Object createObject(ResolvableType objectType, String nestedPath, ValueR return (isOptional && !nestedPath.isEmpty() ? Optional.ofNullable(result) : result); } - private static boolean shouldCreateObject(MethodParameter param) { + /** + * Whether to instantiate the constructor argument of the given type, + * matching its own constructor arguments to bind values. + *

    By default, simple value types, maps, collections, and arrays are + * excluded from nested constructor binding initialization. + * @since 6.1.2 + */ + protected boolean shouldConstructArgument(MethodParameter param) { Class type = param.nestedIfOptional().getNestedParameterType(); - return !(BeanUtils.isSimpleValueType(type) || - Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type) || type.isArray()); + return !BeanUtils.isSimpleValueType(type) && !type.getPackageName().startsWith("java."); + } + + private boolean hasValuesFor(String paramPath, ValueResolver resolver) { + for (String name : resolver.getNames()) { + if (name.startsWith(paramPath + ".")) { + return true; + } + } + return false; + } + + private @Nullable List createList( + String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { + + ResolvableType elementType = type.getNested(2); + SortedSet indexes = getIndexes(paramPath, valueResolver); + if (indexes == null) { + return null; + } + + int lastIndex = Math.max(indexes.last(), 0); + int size = (lastIndex < this.autoGrowCollectionLimit ? lastIndex + 1 : 0); + List list = (List) CollectionFactory.createCollection(paramType, size); + for (int i = 0; i < size; i++) { + list.add(null); + } + + for (int index : indexes) { + String indexedPath = paramPath + "[" + (index != NO_INDEX ? index : "") + "]"; + list.set(Math.max(index, 0), + createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver)); + } + + return list; + } + + private @Nullable Map createMap( + String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { + + ResolvableType elementType = type.getNested(2); + Map map = null; + for (String name : valueResolver.getNames()) { + if (!name.startsWith(paramPath + "[")) { + continue; + } + + int startIdx = paramPath.length() + 1; + int endIdx = name.indexOf(']', startIdx); + boolean quoted = (endIdx - startIdx > 2 && name.charAt(startIdx) == '\'' && name.charAt(endIdx - 1) == '\''); + String key = (quoted ? name.substring(startIdx + 1, endIdx - 1) : name.substring(startIdx, endIdx)); + + if (map == null) { + map = CollectionFactory.createMap(paramType, 16); + } + + String indexedPath = name.substring(0, endIdx + 1); + map.put(key, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver)); + } + + return map; + } + + @SuppressWarnings("unchecked") + private @Nullable V @Nullable [] createArray( + String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { + + ResolvableType elementType = type.getNested(2); + SortedSet indexes = getIndexes(paramPath, valueResolver); + if (indexes == null) { + return null; + } + + int lastIndex = Math.max(indexes.last(), 0); + int size = (lastIndex < this.autoGrowCollectionLimit ? lastIndex + 1: 0); + @Nullable V[] array = (V[]) Array.newInstance(elementType.resolve(), size); + + for (int index : indexes) { + String indexedPath = paramPath + "[" + (index != NO_INDEX ? index : "") + "]"; + array[Math.max(index, 0)] = + createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver); + } + + return array; + } + + private static @Nullable SortedSet getIndexes(String paramPath, ValueResolver valueResolver) { + SortedSet indexes = null; + for (String name : valueResolver.getNames()) { + if (name.startsWith(paramPath + "[")) { + int index; + if (paramPath.length() + 2 == name.length()) { + if (!name.endsWith("[]")) { + continue; + } + index = NO_INDEX; + } + else { + int endIndex = name.indexOf(']', paramPath.length() + 2); + String indexValue = name.substring(paramPath.length() + 1, endIndex); + index = Integer.parseInt(indexValue); + } + indexes = (indexes != null ? indexes : new TreeSet<>()); + indexes.add(index); + } + } + return indexes; + } + + @SuppressWarnings("unchecked") + private @Nullable V createIndexedValue( + String paramPath, Class containerType, ResolvableType elementType, + String indexedPath, ValueResolver valueResolver) { + + Object value = null; + Class elementClass = elementType.resolve(Object.class); + + if (List.class.isAssignableFrom(elementClass)) { + value = createList(indexedPath, elementClass, elementType, valueResolver); + } + else if (Map.class.isAssignableFrom(elementClass)) { + value = createMap(indexedPath, elementClass, elementType, valueResolver); + } + else if (elementClass.isArray()) { + value = createArray(indexedPath, elementClass, elementType, valueResolver); + } + else { + Object rawValue = valueResolver.resolveValue(indexedPath, elementClass); + if (rawValue != null) { + try { + value = convertIfNecessary(rawValue, elementClass); + } + catch (TypeMismatchException ex) { + handleTypeMismatchException(ex, paramPath, containerType, rawValue); + } + } + else { + value = createObject(elementType, indexedPath + ".", valueResolver); + } + } + + return (V) value; + } + + private void handleTypeMismatchException( + TypeMismatchException ex, String paramPath, Class paramType, @Nullable Object value) { + + ex.initPropertyName(paramPath); + getBindingResult().recordFieldValue(paramPath, paramType, value); + getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult()); } 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.getSource() instanceof MethodParameter parameter) { + if (this.targetType != null && this.targetType.getSource() instanceof MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { hints = ValidationAnnotationUtils.determineValidationHints(ann); if (hints != null) { @@ -1140,7 +1289,7 @@ 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()))); + (ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field.toLowerCase(Locale.ROOT)))); } /** @@ -1151,6 +1300,7 @@ protected boolean isAllowed(String field) { * @see #getBindingErrorProcessor * @see BindingErrorProcessor#processMissingFieldError */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected void checkRequiredFields(MutablePropertyValues mpvs) { String[] requiredFields = getRequiredFields(); if (!ObjectUtils.isEmpty(requiredFields)) { @@ -1276,8 +1426,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); } @@ -1285,7 +1434,6 @@ public interface NameResolver { * Strategy for {@link #construct constructor binding} to look up the values * to bind to a given constructor parameter. */ - @FunctionalInterface public interface ValueResolver { /** @@ -1295,8 +1443,17 @@ 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. + *

    Useful for proactive checks whether there are property values nested + * further below the path for a constructor arg. If not then the + * constructor arg can be considered missing and not to be instantiated. + * @since 6.1.2 + */ + Set getNames(); + } diff --git a/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java b/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java index 0b69e3f072d2..34f49f9a4db0 100644 --- a/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java @@ -66,7 +66,7 @@ public void processMissingFieldError(String missingField, BindingResult bindingR @Override public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) { - // Create field error with the exceptions's code, e.g. "typeMismatch". + // Create field error with the code of the exception, for example, "typeMismatch". String field = ex.getPropertyName(); Assert.state(field != null, "No field in exception"); String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field); 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 15655c1cc7fc..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; /** @@ -81,7 +82,7 @@ * {@link MessageCodeFormatter format}. * *

    In order to group all codes into a specific category within your resource bundles, - * e.g. "validation.typeMismatch.name" instead of the default "typeMismatch.name", + * for example, "validation.typeMismatch.name" instead of the default "typeMismatch.name", * consider specifying a {@link #setPrefix prefix} to be applied. * * @author Juergen Hoeller @@ -216,7 +217,7 @@ protected String postProcessMessageCode(String code) { public enum Format implements MessageCodeFormatter { /** - * Prefix the error code at the beginning of the generated message code. e.g.: + * Prefix the error code at the beginning of the generated message code. for example: * {@code errorCode + "." + object name + "." + field} */ PREFIX_ERROR_CODE { @@ -227,7 +228,7 @@ public String format(String errorCode, @Nullable String objectName, @Nullable St }, /** - * Postfix the error code at the end of the generated message code. e.g.: + * Postfix the error code at the end of the generated message code. for example: * {@code object name + "." + field + "." + errorCode} */ POSTFIX_ERROR_CODE { @@ -242,7 +243,7 @@ public String format(String errorCode, @Nullable String objectName, @Nullable St * {@link DefaultMessageCodesResolver#CODE_SEPARATOR}, skipping zero-length or * null elements altogether. */ - public static String toDelimitedString(String... elements) { + public static String toDelimitedString(@Nullable String... elements) { StringJoiner rtn = new StringJoiner(CODE_SEPARATOR); for (String element : elements) { if (StringUtils.hasLength(element)) { 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 a44bbd3ad5c7..cb8ff224cf4b 100644 --- a/spring-context/src/main/java/org/springframework/validation/Errors.java +++ b/spring-context/src/main/java/org/springframework/validation/Errors.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,16 +21,17 @@ 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 * for a specific object. * - *

    Field names are typically properties of the target object (e.g. "name" + *

    Field names are typically properties of the target object (for example, "name" * when binding to a customer object). Implementations may also support nested - * fields in case of nested objects (e.g. "address.street"), in conjunction + * fields in case of nested objects (for example, "address.street"), in conjunction * with subtree navigation via {@link #setNestedPath}: for example, an * {@code AddressValidator} may validate "address", not being aware that this * is a nested object of a top-level customer object. @@ -69,7 +70,7 @@ public interface Errors { *

    The default implementation throws {@code UnsupportedOperationException} * since not all {@code Errors} implementations support nested paths. * @param nestedPath nested path within this object, - * e.g. "address" (defaults to "", {@code null} is also acceptable). + * for example, "address" (defaults to "", {@code null} is also acceptable). * Can end with a dot: both "address" and "address." are valid. * @see #getNestedPath() */ @@ -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 @@ -218,7 +219,7 @@ default void addAllErrors(Errors errors) { /** * Throw the mapped exception with a message summarizing the recorded errors. * @param messageToException a function mapping the message to the exception, - * e.g. {@code IllegalArgumentException::new} or {@code IllegalStateException::new} + * for example, {@code IllegalArgumentException::new} or {@code IllegalStateException::new} * @param the exception type to be thrown * @since 6.1 * @see #toString() @@ -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,16 +382,16 @@ 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); } /** * Return a summary of the recorded errors, - * e.g. for inclusion in an exception message. + * for example, for inclusion in an exception message. * @see #failOnError(Function) */ + @Override String toString(); } 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 e8b03717cf1d..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; } @@ -107,8 +106,7 @@ public boolean equals(@Nullable Object other) { if (!super.equals(other)) { return false; } - FieldError otherError = (FieldError) other; - return (getField().equals(otherError.getField()) && + return (other instanceof FieldError otherError && getField().equals(otherError.getField()) && ObjectUtils.nullSafeEquals(getRejectedValue(), otherError.getRejectedValue()) && isBindingFailure() == otherError.isBindingFailure()); } 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 e8db79267b81..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. @@ -32,10 +32,10 @@ public interface MessageCodeFormatter { /** * Build and return a message code consisting of the given fields, * usually delimited by {@link DefaultMessageCodesResolver#CODE_SEPARATOR}. - * @param errorCode e.g.: "typeMismatch" - * @param objectName e.g.: "user" - * @param field e.g. "age" - * @return concatenated message code, e.g.: "typeMismatch.user.age" + * @param errorCode for example: "typeMismatch" + * @param objectName for example: "user" + * @param field for example, "age" + * @return concatenated message code, for example: "typeMismatch.user.age" * @see DefaultMessageCodesResolver.Format */ String format(String errorCode, @Nullable String objectName, @Nullable String field); 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 53aee336d350..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"); @@ -94,7 +94,7 @@ public void wrap(Object source) { * (typically {@link org.springframework.beans.PropertyAccessException}) * or a Bean Validation {@link jakarta.validation.ConstraintViolation}. *

    The cause of the outermost exception will be introspected as well, - * e.g. the underlying conversion exception or exception thrown from a setter + * for example, the underlying conversion exception or exception thrown from a setter * (instead of having to unwrap the {@code PropertyAccessException} in turn). * @return the source object of the given type * @throws IllegalArgumentException if no such source object is available @@ -119,7 +119,7 @@ else if (this.source instanceof Throwable throwable) { * (typically {@link org.springframework.beans.PropertyAccessException}) * or a Bean Validation {@link jakarta.validation.ConstraintViolation}. *

    The cause of the outermost exception will be introspected as well, - * e.g. the underlying conversion exception or exception thrown from a setter + * for example, the underlying conversion exception or exception thrown from a setter * (instead of having to unwrap the {@code PropertyAccessException} in turn). * @return whether this error has been caused by a source object of the given type * @since 5.0.4 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 21c616ffdd99..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,7 +147,7 @@ public Object getFieldValue(String field) { } @Override - 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..263558009cd1 100644 --- a/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java +++ b/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java @@ -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,7 +246,7 @@ 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); diff --git a/spring-context/src/main/java/org/springframework/validation/Validator.java b/spring-context/src/main/java/org/springframework/validation/Validator.java index bea33261aeb7..1aa251a368a3 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -96,7 +96,7 @@ public interface Validator { * Validate the given {@code target} object individually. *

    Delegates to the common {@link #validate(Object, Errors)} method. * The returned {@link Errors errors} instance can be used to report - * any resulting validation errors for the specific target object, e.g. + * any resulting validation errors for the specific target object, for example, * {@code if (validator.validateObject(target).hasErrors()) ...} or * {@code validator.validateObject(target).failOnError(IllegalStateException::new));}. *

    Note: This validation call comes with limitations in the {@link Errors} diff --git a/spring-context/src/main/java/org/springframework/validation/annotation/Validated.java b/spring-context/src/main/java/org/springframework/validation/annotation/Validated.java index 02a651ac8fef..d42286a00872 100644 --- a/spring-context/src/main/java/org/springframework/validation/annotation/Validated.java +++ b/spring-context/src/main/java/org/springframework/validation/annotation/Validated.java @@ -27,7 +27,7 @@ * specification of validation groups. Designed for convenient use with * Spring's JSR-303 support but not JSR-303 specific. * - *

    Can be used e.g. with Spring MVC handler methods arguments. + *

    Can be used, for example, with Spring MVC handler methods arguments. * Supported through {@link org.springframework.validation.SmartValidator}'s * validation hint concept, with validation group classes acting as hint objects. * 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 0f55e8406fa6..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 @@ -1,13 +1,11 @@ /** * Support classes for annotation-based constraint evaluation, - * e.g. using a JSR-303 Bean Validation provider. + * for example, using a JSR-303 Bean Validation provider. * *

    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 d13639a0c08c..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 @@ -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,31 +18,38 @@ import java.util.Collection; import java.util.HashSet; +import java.util.Map; +import java.util.Optional; import java.util.Set; import jakarta.validation.ConstraintValidator; import jakarta.validation.NoProviderFoundException; import jakarta.validation.Validation; import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import jakarta.validation.metadata.BeanDescriptor; import jakarta.validation.metadata.ConstraintDescriptor; -import jakarta.validation.metadata.ConstructorDescriptor; -import jakarta.validation.metadata.MethodDescriptor; +import jakarta.validation.metadata.ContainerElementTypeDescriptor; +import jakarta.validation.metadata.ExecutableDescriptor; import jakarta.validation.metadata.MethodType; import jakarta.validation.metadata.ParameterDescriptor; 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; +import org.springframework.aot.hint.ReflectionHints; 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; +import org.springframework.core.ResolvableType; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; /** * AOT {@code BeanRegistrationAotProcessor} that adds additional hints @@ -61,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); } @@ -75,13 +81,11 @@ 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() { - try { - return Validation.buildDefaultValidatorFactory().getValidator(); + private static @Nullable Validator getValidatorIfAvailable() { + try (ValidatorFactory validator = Validation.buildDefaultValidatorFactory()) { + return validator.getValidator(); } catch (NoProviderFoundException ex) { logger.info("No Bean Validation provider available - skipping validation constraint hint inference"); @@ -89,70 +93,150 @@ private static Validator getValidatorIfAvailable() { } } - @Nullable - public static BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public static @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { if (validator == null) { return null; } + Class beanClass = registeredBean.getBeanClass(); + Set> validatedClasses = new HashSet<>(); + Set>> constraintValidatorClasses = new HashSet<>(); + + processAheadOfTime(beanClass, new HashSet<>(), validatedClasses, constraintValidatorClasses); + + if (!validatedClasses.isEmpty() || !constraintValidatorClasses.isEmpty()) { + return new AotContribution(validatedClasses, constraintValidatorClasses); + } + return null; + } + + private static void processAheadOfTime(Class clazz, Set> visitedClasses, Set> validatedClasses, + Set>> constraintValidatorClasses) { + + Assert.notNull(validator, "Validator cannot be null"); + + if (!visitedClasses.add(clazz)) { + return; + } + BeanDescriptor descriptor; try { - descriptor = validator.getConstraintsForClass(registeredBean.getBeanClass()); + descriptor = validator.getConstraintsForClass(clazz); } - catch (RuntimeException ex) { - if (KotlinDetector.isKotlinType(registeredBean.getBeanClass()) && ex instanceof ArrayIndexOutOfBoundsException) { + catch (RuntimeException | LinkageError ex) { + String className = clazz.getName(); + if (KotlinDetector.isKotlinType(clazz) && ex instanceof ArrayIndexOutOfBoundsException) { // See https://hibernate.atlassian.net/browse/HV-1796 and https://youtrack.jetbrains.com/issue/KT-40857 - logger.warn("Skipping validation constraint hint inference for bean " + registeredBean.getBeanName() + - " due to an ArrayIndexOutOfBoundsException at validator level"); + if (logger.isWarnEnabled()) { + logger.warn("Skipping validation constraint hint inference for class " + className + + " due to an ArrayIndexOutOfBoundsException at validator level"); + } } - else if (ex instanceof TypeNotPresentException) { - logger.debug("Skipping validation constraint hint inference for bean " + - registeredBean.getBeanName() + " due to a TypeNotPresentException at validator level: " + ex.getMessage()); + else if (ex instanceof TypeNotPresentException || ex instanceof NoClassDefFoundError) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping validation constraint hint inference for class %s due to a %s for %s" + .formatted(className, ex.getClass().getSimpleName(), ex.getMessage())); + } } else { - logger.warn("Skipping validation constraint hint inference for bean " + - registeredBean.getBeanName(), ex); + if (logger.isWarnEnabled()) { + logger.warn("Skipping validation constraint hint inference for class " + className, ex); + } } - return null; + return; } - Set> constraintDescriptors = new HashSet<>(); - for (MethodDescriptor methodDescriptor : descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER)) { - for (ParameterDescriptor parameterDescriptor : methodDescriptor.getParameterDescriptors()) { - constraintDescriptors.addAll(parameterDescriptor.getConstraintDescriptors()); - } + processExecutableDescriptor(descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER), constraintValidatorClasses); + processExecutableDescriptor(descriptor.getConstrainedConstructors(), constraintValidatorClasses); + processPropertyDescriptors(descriptor.getConstrainedProperties(), constraintValidatorClasses); + if (!constraintValidatorClasses.isEmpty() && shouldProcess(clazz)) { + validatedClasses.add(clazz); } - for (ConstructorDescriptor constructorDescriptor : descriptor.getConstrainedConstructors()) { - for (ParameterDescriptor parameterDescriptor : constructorDescriptor.getParameterDescriptors()) { - constraintDescriptors.addAll(parameterDescriptor.getConstraintDescriptors()); + + ReflectionUtils.doWithFields(clazz, field -> { + Class type = field.getType(); + if (Iterable.class.isAssignableFrom(type) || Optional.class.isAssignableFrom(type)) { + ResolvableType resolvableType = ResolvableType.forField(field); + Class genericType = resolvableType.getGeneric(0).toClass(); + if (shouldProcess(genericType)) { + validatedClasses.add(clazz); + processAheadOfTime(genericType, visitedClasses, validatedClasses, constraintValidatorClasses); + } + } + if (Map.class.isAssignableFrom(type)) { + ResolvableType resolvableType = ResolvableType.forField(field); + Class keyGenericType = resolvableType.getGeneric(0).toClass(); + Class valueGenericType = resolvableType.getGeneric(1).toClass(); + if (shouldProcess(keyGenericType)) { + validatedClasses.add(clazz); + processAheadOfTime(keyGenericType, visitedClasses, validatedClasses, constraintValidatorClasses); + } + if (shouldProcess(valueGenericType)) { + validatedClasses.add(clazz); + processAheadOfTime(valueGenericType, visitedClasses, validatedClasses, constraintValidatorClasses); + } + } + }); + } + + private static boolean shouldProcess(Class clazz) { + return !clazz.getCanonicalName().startsWith("java."); + } + + private static void processExecutableDescriptor(Set executableDescriptors, + Collection>> constraintValidatorClasses) { + + for (ExecutableDescriptor executableDescriptor : executableDescriptors) { + for (ParameterDescriptor parameterDescriptor : executableDescriptor.getParameterDescriptors()) { + for (ConstraintDescriptor constraintDescriptor : parameterDescriptor.getConstraintDescriptors()) { + constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses()); + } + for (ContainerElementTypeDescriptor typeDescriptor : parameterDescriptor.getConstrainedContainerElementTypes()) { + for (ConstraintDescriptor constraintDescriptor : typeDescriptor.getConstraintDescriptors()) { + constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses()); + } + } } } - for (PropertyDescriptor propertyDescriptor : descriptor.getConstrainedProperties()) { - constraintDescriptors.addAll(propertyDescriptor.getConstraintDescriptors()); - } - if (!constraintDescriptors.isEmpty()) { - return new AotContribution(constraintDescriptors); + } + + private static void processPropertyDescriptors(Set propertyDescriptors, + Collection>> constraintValidatorClasses) { + + for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { + for (ConstraintDescriptor constraintDescriptor : propertyDescriptor.getConstraintDescriptors()) { + constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses()); + } + for (ContainerElementTypeDescriptor typeDescriptor : propertyDescriptor.getConstrainedContainerElementTypes()) { + for (ConstraintDescriptor constraintDescriptor : typeDescriptor.getConstraintDescriptors()) { + constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses()); + } + } } - return null; } } private static class AotContribution implements BeanRegistrationAotContribution { - private final Collection> constraintDescriptors; + private final Collection> validatedClasses; + private final Collection>> constraintValidatorClasses; - public AotContribution(Collection> constraintDescriptors) { - this.constraintDescriptors = constraintDescriptors; + public AotContribution(Collection> validatedClasses, + Collection>> constraintValidatorClasses) { + + this.validatedClasses = validatedClasses; + this.constraintValidatorClasses = constraintValidatorClasses; } @Override public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { - for (ConstraintDescriptor constraintDescriptor : this.constraintDescriptors) { - for (Class constraintValidatorClass : constraintDescriptor.getConstraintValidatorClasses()) { - generationContext.getRuntimeHints().reflection().registerType(constraintValidatorClass, - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - } + ReflectionHints hints = generationContext.getRuntimeHints().reflection(); + for (Class validatedClass : this.validatedClasses) { + 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 a15f0726fdd0..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-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,14 @@ import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; @@ -36,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; @@ -48,8 +51,7 @@ 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.ClassUtils; +import org.springframework.util.Assert; import org.springframework.util.function.SingletonSupplier; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; @@ -193,7 +195,7 @@ public ParameterNameDiscoverer getParameterNameDiscoverer() { *

  • {@link Conventions#getVariableNameForReturnType(Method, Class, Object)} * for a return type * - * If a name cannot be determined, e.g. a return value with insufficient + * If a name cannot be determined, for example, a return value with insufficient * type information, then it defaults to one of: *