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/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 000000000000..413e9b2f6571 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,83 @@ +name: 'Build' +description: 'Builds the project, optionally publishing it to a local deployment repository' +inputs: + commercial-release-repository-url: + description: 'URL of the release repository' + required: false + commercial-repository-password: + description: 'Password for authentication with the commercial repository' + required: false + commercial-repository-username: + description: 'Username for authentication with the commercial repository' + required: false + commercial-snapshot-repository-url: + description: 'URL of the snapshot repository' + required: false + 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: '25' + 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 + env: + COMMERCIAL_RELEASE_REPO_URL: ${{ inputs.commercial-release-repository-url }} + COMMERCIAL_REPO_PASSWORD: ${{ inputs.commercial-repository-password }} + COMMERCIAL_REPO_USERNAME: ${{ inputs.commercial-repository-username }} + COMMERCIAL_SNAPSHOT_REPO_URL: ${{ inputs.commercial-snapshot-repository-url }} + run: ./gradlew check antora + - name: Publish + id: publish + if: ${{ inputs.publish == 'true' }} + shell: bash + env: + COMMERCIAL_RELEASE_REPO_URL: ${{ inputs.commercial-release-repository-url }} + COMMERCIAL_REPO_PASSWORD: ${{ inputs.commercial-repository-password }} + COMMERCIAL_REPO_USERNAME: ${{ inputs.commercial-repository-username }} + COMMERCIAL_SNAPSHOT_REPO_URL: ${{ inputs.commercial-snapshot-repository-url }} + 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..275ff632f58a --- /dev/null +++ b/.github/actions/create-github-release/action.yml @@ -0,0 +1,34 @@ +name: Create GitHub Release +description: 'Create the release on GitHub with a changelog' +inputs: + commercial: + description: 'Whether to generate the changelog for the commercial release' + required: true + latest: + description: 'Whether the release is the latest release' + required: false + default: 'false' + 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@f7d7a87a3e7c627ecb8c26cf086c38ac5a939721 #v0.0.14 + with: + config-file: ${{ inputs.commercial && '.github/actions/create-github-release/changelog-generator-commercial.yml' || '.github/actions/create-github-release/changelog-generator-oss.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' || format('--latest={0}', inputs.latest) }} diff --git a/.github/actions/create-github-release/changelog-generator-commercial.yml b/.github/actions/create-github-release/changelog-generator-commercial.yml new file mode 100644 index 000000000000..69f78ed5c860 --- /dev/null +++ b/.github/actions/create-github-release/changelog-generator-commercial.yml @@ -0,0 +1,33 @@ +changelog: + repository: spring-projects/spring-framework-commercial + sections: + - title: ":warning: Attention Required" + labels: + - "for: upgrade-attention" + summary: + mode: "member-comment" + config: + prefix: "Attention Required:" + - 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" + - "rstoyanchev" + - "sbrannen" + - "sdeleuze" + - "snicoll" diff --git a/.github/actions/create-github-release/changelog-generator-oss.yml b/.github/actions/create-github-release/changelog-generator-oss.yml new file mode 100644 index 000000000000..31f7d2e1cd8e --- /dev/null +++ b/.github/actions/create-github-release/changelog-generator-oss.yml @@ -0,0 +1,33 @@ +changelog: + repository: spring-projects/spring-framework + sections: + - title: ":warning: Attention Required" + labels: + - "for: upgrade-attention" + summary: + mode: "member-comment" + config: + prefix: "Attention Required:" + - 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" + - "rstoyanchev" + - "sbrannen" + - "sdeleuze" + - "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..955f96d0685a --- /dev/null +++ b/.github/actions/prepare-gradle-build/action.yml @@ -0,0 +1,53 @@ +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: '25' +runs: + using: composite + steps: + - name: Set Up Java + uses: actions/setup-java@v5 + 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' && '25' || '' }} + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # 6.2.0 + with: + cache-provider: basic + cache-read-only: false + develocity-access-key: ${{ inputs.develocity-access-key }} + develocity-token-expiry: 4 + - name: Configure Gradle Properties + shell: bash + run: | + echo 'systemProp.user.name=spring-builds+github' >> $GRADLE_USER_HOME/gradle.properties + echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $GRADLE_USER_HOME/gradle.properties + echo 'org.gradle.daemon=false' >> $GRADLE_USER_HOME/gradle.properties + - name: Configure Toolchain Properties + if: ${{ inputs.java-toolchain == 'true' }} + shell: bash + run: | + echo toolchainVersion=${{ inputs.java-version }} >> $GRADLE_USER_HOME/gradle.properties + echo systemProp.org.gradle.java.installations.auto-detect=false >> $GRADLE_USER_HOME/gradle.properties + echo systemProp.org.gradle.java.installations.auto-download=false >> $GRADLE_USER_HOME/gradle.properties + echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $GRADLE_USER_HOME/gradle.properties \ No newline at end of file diff --git a/.github/actions/print-jvm-thread-dumps/action.yml b/.github/actions/print-jvm-thread-dumps/action.yml index bab22e54897a..bcaebf3676aa 100644 --- a/.github/actions/print-jvm-thread-dumps/action.yml +++ b/.github/actions/print-jvm-thread-dumps/action.yml @@ -1,5 +1,5 @@ name: Print JVM thread dumps -description: Prints a thread dump for all running JVMs +description: 'Prints a thread dump for all running JVMs' runs: using: composite steps: @@ -7,7 +7,7 @@ runs: shell: bash run: | for jvm_pid in $(jps -q -J-XX:+PerfDisableSharedMem); do - jcmd $java_pid Thread.print + jcmd $jvm_pid Thread.print done - if: ${{ runner.os == 'Windows' }} shell: powershell diff --git a/.github/actions/release-train-build/action.yml b/.github/actions/release-train-build/action.yml new file mode 100644 index 000000000000..0c7bd36f2e8c --- /dev/null +++ b/.github/actions/release-train-build/action.yml @@ -0,0 +1,7 @@ +name: Build Release +runs: + using: composite + steps: + - name: Build Release + shell: bash + run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository publishAllPublicationsToDeploymentRepository \ No newline at end of file diff --git a/.github/actions/release-train-build/deployment-spec.yml b/.github/actions/release-train-build/deployment-spec.yml new file mode 100644 index 000000000000..7fa3e1849196 --- /dev/null +++ b/.github/actions/release-train-build/deployment-spec.yml @@ -0,0 +1,16 @@ +artifactory: + artifacts: + - pattern: "/**/framework-api-*.zip" + properties: + zip.deployed: "false" + zip.name: "spring-framework" + - pattern: "/**/framework-api-*-docs.zip" + properties: + zip.type: "docs" + - pattern: "/**/framework-api-*-schema.zip" + properties: + zip.type: "schema" +maven-central: + excludes: + - "org/springframework/framework-api/**" + - "org/springframework/framework-docs/**" diff --git a/.github/actions/release-train-test/action.yml b/.github/actions/release-train-test/action.yml new file mode 100644 index 000000000000..937f68ecf362 --- /dev/null +++ b/.github/actions/release-train-test/action.yml @@ -0,0 +1,7 @@ +name: Test Release +runs: + using: composite + steps: + - name: Test Release + shell: bash + run: ./gradlew check \ No newline at end of file diff --git a/.github/actions/send-notification/action.yml b/.github/actions/send-notification/action.yml index 9582d44ed154..b379e67897d1 100644 --- a/.github/actions/send-notification/action.yml +++ b/.github/actions/send-notification/action.yml @@ -1,33 +1,39 @@ -name: Send notification -description: Sends a Google Chat message as a notification of the job's outcome +name: Send Notification +description: 'Sends a Google Chat message as a notification of the job''s outcome' inputs: - webhook-url: - description: 'Google Chat Webhook URL' - required: true - status: - description: 'Status of the job' - required: true 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: - - shell: bash + - 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" - - shell: bash + - 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 - - shell: bash + - 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 - - shell: bash + - 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/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/workflow-generator.yml b/.github/workflow-generator.yml new file mode 100644 index 000000000000..30afeb9d298f --- /dev/null +++ b/.github/workflow-generator.yml @@ -0,0 +1,18 @@ +workflow: + generator: + project: + java: + versions: + primary: 25 + workflows: + release-train: + build: + env: + COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME + COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD + COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL + test: + env: + COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME + COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD + COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL \ No newline at end of file diff --git a/.github/workflows/backport-bot.yml b/.github/workflows/backport-bot.yml index 4d025ece2ceb..8def7183d7fb 100644 --- a/.github/workflows/backport-bot.yml +++ b/.github/workflows/backport-bot.yml @@ -1,5 +1,4 @@ name: Backport Bot - on: issues: types: [labeled] @@ -8,27 +7,15 @@ on: push: branches: - '*.x' -permissions: - contents: read jobs: - build: + backport-issue: permissions: contents: read issues: write pull-requests: write runs-on: ubuntu-latest steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Set up Java - uses: actions/setup-java@v4 + - name: Create Backport Issue + uses: spring-io/backport-bot@v0.0.2 with: - 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) }} - run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT" + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index ff1a1d2f39bd..4d0e8ad291c8 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -1,4 +1,4 @@ -name: Build and deploy snapshot +name: Build and Deploy Snapshot on: push: branches: @@ -7,56 +7,71 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: build-and-deploy-snapshot: - if: ${{ github.repository == 'spring-projects/spring-framework' }} - name: Build and deploy snapshot - runs-on: ubuntu-latest + name: Build and Deploy Snapshot + if: ${{ github.repository == 'spring-projects/spring-framework' || github.repository == 'spring-projects/spring-framework-commercial' }} + runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} + timeout-minutes: 60 steps: - - name: Set up Java - uses: actions/setup-java@v4 + - name: Check Out Code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build with: - distribution: 'liberica' - java-version: 17 - - name: Check out code - uses: actions/checkout@v4 - - name: Set up Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 - with: - cache-read-only: false - - 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: Build and publish - id: build - env: - CI: 'true' - GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository + commercial-release-repository-url: ${{ vars.COMMERCIAL_RELEASE_REPO_URL }} + commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }} + commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }} + commercial-snapshot-repository-url: ${{ vars.COMMERCIAL_SNAPSHOT_REPO_URL }} + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + publish: true - name: Deploy - uses: spring-io/artifactory-deploy-action@v0.0.1 + uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4 with: - uri: 'https://repo.spring.io' - username: ${{ secrets.ARTIFACTORY_USERNAME }} - password: ${{ secrets.ARTIFACTORY_PASSWORD }} - build-name: ${{ format('spring-framework-{0}', github.ref_name)}} - repository: 'libs-snapshot-local' - folder: 'deployment-repository' - signing-key: ${{ secrets.GPG_PRIVATE_KEY }} - signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} 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 - - name: Send notification - uses: ./.github/actions/send-notification + build-name: ${{ vars.COMMERCIAL && format('spring-framework-commercial-{0}', '7.1.x') || format('spring-framework-{0}', '7.1.x') }} + folder: 'deployment-repository' + project: ${{ vars.COMMERCIAL && 'spring' }} + repository: ${{ vars.COMMERCIAL && 'spring-enterprise-maven-dev-local' || 'libs-snapshot-local' }} + uri: ${{ vars.COMMERCIAL_DEPLOY_REPO_URL || 'https://repo.spring.io' }} + username: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_USERNAME || secrets.ARTIFACTORY_USERNAME }} + password: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_PASSWORD || secrets.ARTIFACTORY_PASSWORD }} + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + - name: Send Notification if: always() + uses: ./.github/actions/send-notification with: - webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }} + run-name: ${{ format('{0} | Linux | Java 25', github.ref_name) }} status: ${{ job.status }} - build-scan-url: ${{ steps.build.outputs.build-scan-url }} - run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} \ No newline at end of file + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + outputs: + version: ${{ steps.build-and-publish.outputs.version }} + trigger-docs-build: + name: Trigger Docs Build + needs: build-and-deploy-snapshot + if: ${{ !vars.COMMERCIAL }} # remove when commercial support + permissions: + actions: write + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} + steps: + - name: Run Deploy Docs Workflow + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml --repo ${{ github.repository }} -r docs-build -f build-refname=${{ github.ref_name }} + verify: + name: Verify + needs: build-and-deploy-snapshot + uses: ./.github/workflows/verify.yml + secrets: + commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }} + commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }} + google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + opensource-repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + opensource-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 index fc5a448892c0..8b6b30a01b85 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -1,43 +1,24 @@ name: Build Pull Request on: pull_request - permissions: contents: read - jobs: build: - name: Build pull request - runs-on: ubuntu-latest + name: Build Pull Request if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ubuntu-latest steps: - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'liberica' - - - name: Check out code - uses: actions/checkout@v4 - - - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2 - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 - + - name: Check Out Code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Build - env: - CI: 'true' - GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' - run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build - - - name: Print JVM thread dumps when cancelled - uses: ./.github/actions/print-jvm-thread-dumps + id: build + uses: ./.github/actions/build + - name: Print JVM Thread Dumps When Cancelled if: cancelled() - - - name: Upload build reports - uses: actions/upload-artifact@v4 + uses: ./.github/actions/print-jvm-thread-dumps + - name: Upload Build Reports if: failure() + uses: actions/upload-artifact@v7 with: name: build-reports - path: '**/build/reports/' + path: '**/build/reports/' \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 707a26103639..f4dc6b91b5b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,76 +2,60 @@ name: CI on: schedule: - cron: '30 9 * * *' -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} +permissions: + contents: read jobs: ci: - if: ${{ github.repository == 'spring-projects/spring-framework' }} + name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' + if: ${{ github.repository == 'spring-projects/spring-framework' || github.repository == 'spring-projects/spring-framework-commercial' }} + runs-on: ${{ matrix.os.id }} + timeout-minutes: 60 strategy: matrix: os: - - id: ubuntu-latest + - id: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name: Linux java: - version: 17 - toolchain: false + toolchain: true - version: 21 toolchain: true + - version: 25 + toolchain: false + - version: 26 + toolchain: true exclude: - os: name: Linux java: - version: 17 - name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' - runs-on: ${{ matrix.os.id }} + version: 25 steps: - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: 'liberica' - java-version: | - ${{ matrix.java.version }} - ${{ matrix.java.toolchain && '17' || '' }} - 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: Set up Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 - with: - cache-read-only: false - - 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: ${{ matrix.java.toolchain }} - shell: bash - run: | - echo toolchainVersion=${{ matrix.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', matrix.java.version) }} >> $HOME/.gradle/gradle.properties + - name: Check Out Code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Build id: build - env: - CI: 'true' - GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - run: ./gradlew check antora - - name: Send notification - uses: ./.github/actions/send-notification + uses: ./.github/actions/build + with: + commercial-release-repository-url: ${{ vars.COMMERCIAL_RELEASE_REPO_URL }} + commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }} + commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }} + commercial-snapshot-repository-url: ${{ vars.COMMERCIAL_SNAPSHOT_REPO_URL }} + 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: - webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} - status: ${{ job.status }} 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) }} \ No newline at end of file + 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 deleted file mode 100644 index 1d66f04806b0..000000000000 --- a/.github/workflows/deploy-docs.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Deploy Docs -on: - push: - branches: - - 'main' - - '*.x' - - '!gh-pages' - tags: - - 'v*' - repository_dispatch: - types: request-build-reference # legacy - workflow_dispatch: -permissions: - actions: write -jobs: - build: - runs-on: ubuntu-latest - if: github.repository_owner == 'spring-projects' - steps: - - name: Check out code - uses: actions/checkout@v4 - with: - ref: docs-build - fetch-depth: 1 - - name: Dispatch (partial build) - if: github.ref_type == 'branch' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }} - - name: Dispatch (full build) - if: github.ref_type == 'tag' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml deleted file mode 100644 index cf2c086a063c..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@v4 - - uses: gradle/wrapper-validation-action@v2 diff --git a/.github/workflows/publish-milestone.yml b/.github/workflows/publish-milestone.yml new file mode 100644 index 000000000000..6377f4fb2197 --- /dev/null +++ b/.github/workflows/publish-milestone.yml @@ -0,0 +1,39 @@ +name: Release Milestone +on: + push: + tags: + - v7.1.0-M[1-9] + - v7.1.0-RC[1-9] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + trigger-docs-build: + name: Trigger Docs Build + permissions: + actions: write + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Determine Version + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Run Deploy Docs Workflow + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml --repo ${{ github.repository }} -r docs-build -f build-refname=${{ github.ref_name }} + create-github-release: + name: Create GitHub Release + needs: + - trigger-docs-build + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@v6 + - name: Create GitHub Release + uses: ./.github/actions/create-github-release + with: + commercial: ${{ vars.COMMERCIAL }} + milestone: ${{ needs.trigger-docs-build.outputs.version }} + pre-release: true + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 000000000000..d635ca914121 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,38 @@ +name: Release +on: + push: + tags: + - v7.1.[0-9]+ +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + trigger-docs-build: + name: Trigger Docs Build + permissions: + actions: write + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Determine Version + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Run Deploy Docs Workflow + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml --repo ${{ github.repository }} -r docs-build -f build-refname=${{ github.ref_name }} + create-github-release: + name: Create GitHub Release + needs: + - trigger-docs-build + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@v6 + - name: Create GitHub Release + uses: ./.github/actions/create-github-release + with: + commercial: ${{ vars.COMMERCIAL }} + latest: true + milestone: ${{ needs.trigger-docs-build.outputs.version }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/.github/workflows/release-train-build.yml b/.github/workflows/release-train-build.yml new file mode 100644 index 000000000000..e2789a7dc1cc --- /dev/null +++ b/.github/workflows/release-train-build.yml @@ -0,0 +1,93 @@ +# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit. +# To update it, modify .github/workflow-generator.yml as needed and re-run the generator. + +name: "Release Train – Build" +run-name: "${{ inputs.callback-ref }} – Build" +"on": + workflow_dispatch: + inputs: + callback: + description: "Repository to which a callback should be made upon completion" + required: true + type: "string" + callback-ref: + description: "Ref in the callback repository to which a callback should be made upon completion" + required: true + type: "string" + release-train-maven-repository-url: + description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train" + required: true + type: "string" +permissions: + contents: "read" +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" +jobs: + build-release: + name: "Build Release" + runs-on: "ubuntu22-2-8" + steps: + - name: "Prevent Re-runs" + id: "prevent-re-runs" + run: |- + if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then + echo "Re-runs are prohibited. Use the 'Release Train – Retry' workflow to retry build failures" + exit 1 + fi + - name: "Set up Java" + id: "set-up-java" + uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0 + with: + distribution: "liberica" + java-version: "25" + - name: "Check Out Code" + id: "check-out-code" + uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0 + - name: "Build Release" + id: "build-release" + uses: "./.github/actions/release-train-build" + env: + COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}" + COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}" + COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}" + RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}" + RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}" + RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}" + - name: "Upload Deployment Repository" + id: "upload-deployment-repository" + uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1 + with: + name: "deployment-repository" + path: "deployment-repository/**" + - name: "Upload Deployment Spec" + id: "upload-deployment-spec" + uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1 + with: + archive: "false" + if-no-files-found: "ignore" + name: "deployment-spec" + path: ".github/actions/release-train-build/deployment-spec.yml" + - name: "Save Build System Caches" + id: "save-build-system-caches" + uses: "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0 + with: + key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}" + path: |- + ~/.gradle/caches + ~/.gradle/wrapper + - name: "Send Callback" + id: "send-callback" + if: "${{ !cancelled() }}" + env: + GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}" + run: |- + gh workflow run callback \ + --repo ${{ inputs.callback }} \ + --ref ${{ inputs.callback-ref }} \ + --field commit-hash=${{ steps.check-out-code.outputs.commit }} \ + --field deployment-repository-artifact-identifier=${{ steps.upload-deployment-repository.outputs.artifact-id }} \ + --field deployment-spec-artifact-identifier=${{ steps.upload-deployment-spec.outputs.artifact-id }} \ + --field release-branch=${{ github.ref_name }} \ + --field release-repository=${{ github.repository }} \ + --field result=${{ job.status == 'success' && 'built' || 'build-failed' }} \ + --field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/release-train-join.yml b/.github/workflows/release-train-join.yml new file mode 100644 index 000000000000..729914b3f907 --- /dev/null +++ b/.github/workflows/release-train-join.yml @@ -0,0 +1,55 @@ +# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit. +# To update it, modify .github/workflow-generator.yml as needed and re-run the generator. + +name: "Release Train – Join" +run-name: "${{ inputs.release-train }} – Join" +"on": + workflow_dispatch: + inputs: + deployment-destination: + description: "Destination to which the release should be deployed" + options: + - "Maven Central" + - "Spring Enterprise" + required: true + type: "choice" + release-train: + description: "Release train" + required: true + type: "string" + release-train-repository: + default: "spring-io/release-train" + description: "Release train repository" + required: true + type: "string" +permissions: + contents: "none" +jobs: + join-release-train: + name: "Join Release Train" + runs-on: "ubuntu-latest" + steps: + - name: "Join Release Train" + id: "join-release-train" + env: + GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}" + run: |- + run_url=$( + gh workflow run join \ + --repo ${{ inputs.release-train-repository }} \ + --ref ${{ inputs.release-train }} \ + --field commit-hash=${{ github.sha }} \ + --field deployment-destination=${{ inputs.deployment-destination == 'Maven Central' && 'maven-central' || 'spring-enterprise' }} \ + --field release-branch=${{ github.ref_name }} \ + --field release-repository=${{ github.repository }} + ) + echo "Dispatched workflow run. Waiting for $run_url to complete." + run_id=${run_url##*/} + watch_exit_code=0 + gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$? + if [[ $watch_exit_code -eq 0 ]]; then + echo "Workflow run succeeded." + else + echo "Workflow run failed." + fi + exit $watch_exit_code diff --git a/.github/workflows/release-train-leave.yml b/.github/workflows/release-train-leave.yml new file mode 100644 index 000000000000..bc0be6711a70 --- /dev/null +++ b/.github/workflows/release-train-leave.yml @@ -0,0 +1,46 @@ +# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit. +# To update it, modify .github/workflow-generator.yml as needed and re-run the generator. + +name: "Release Train – Leave" +run-name: "${{ inputs.release-train }} – Leave" +"on": + workflow_dispatch: + inputs: + release-train: + description: "Release train" + required: true + type: "string" + release-train-repository: + default: "spring-io/release-train" + description: "Release train repository" + required: true + type: "string" +permissions: + contents: "none" +jobs: + leave: + name: "Leave" + runs-on: "ubuntu-latest" + steps: + - name: "Leave" + id: "leave" + env: + GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}" + run: |- + run_url=$( + gh workflow run leave \ + --repo ${{ inputs.release-train-repository }} \ + --ref ${{ inputs.release-train }} \ + --field release-branch=${{ github.ref_name }} \ + --field release-repository=${{ github.repository }} + ) + echo "Dispatched workflow run. Waiting for $run_url to complete." + run_id=${run_url##*/} + watch_exit_code=0 + gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$? + if [[ $watch_exit_code -eq 0 ]]; then + echo "Workflow run succeeded." + else + echo "Workflow run failed." + fi + exit $watch_exit_code diff --git a/.github/workflows/release-train-ready.yml b/.github/workflows/release-train-ready.yml new file mode 100644 index 000000000000..794a29a6292f --- /dev/null +++ b/.github/workflows/release-train-ready.yml @@ -0,0 +1,47 @@ +# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit. +# To update it, modify .github/workflow-generator.yml as needed and re-run the generator. + +name: "Release Train – Ready" +run-name: "${{ inputs.release-train }} – Ready" +"on": + workflow_dispatch: + inputs: + release-train: + description: "Release train" + required: true + type: "string" + release-train-repository: + default: "spring-io/release-train" + description: "Release train repository" + required: true + type: "string" +permissions: + contents: "none" +jobs: + ready: + name: "Ready" + runs-on: "ubuntu-latest" + steps: + - name: "Ready" + id: "ready" + env: + GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}" + run: |- + run_url=$( + gh workflow run ready \ + --repo ${{ inputs.release-train-repository }} \ + --ref ${{ inputs.release-train }} \ + --field commit-hash=${{ github.sha }} \ + --field release-branch=${{ github.ref_name }} \ + --field release-repository=${{ github.repository }} + ) + echo "Dispatched workflow run. Waiting for $run_url to complete." + run_id=${run_url##*/} + watch_exit_code=0 + gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$? + if [[ $watch_exit_code -eq 0 ]]; then + echo "Workflow run succeeded." + else + echo "Workflow run failed." + fi + exit $watch_exit_code diff --git a/.github/workflows/release-train-retry.yml b/.github/workflows/release-train-retry.yml new file mode 100644 index 000000000000..539ccfd2504c --- /dev/null +++ b/.github/workflows/release-train-retry.yml @@ -0,0 +1,34 @@ +# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit. +# To update it, modify .github/workflow-generator.yml as needed and re-run the generator. + +name: "Release Train – Retry" +run-name: "${{ inputs.release-train }} – Retry" +"on": + workflow_dispatch: + inputs: + release-train: + description: "Release train" + required: true + type: "string" + release-train-repository: + default: "spring-io/release-train" + description: "Release train repository" + required: true + type: "string" +permissions: + contents: "none" +jobs: + trigger-retry: + name: "Trigger Retry" + runs-on: "ubuntu-latest" + steps: + - name: "Trigger Retry" + id: "trigger-retry" + env: + GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}" + run: |- + gh workflow run retry \ + --repo ${{ inputs.release-train-repository }} \ + --ref ${{ inputs.release-train }} \ + --field release-branch=${{ github.ref_name }} \ + --field release-repository=${{ github.repository }} diff --git a/.github/workflows/release-train-test.yml b/.github/workflows/release-train-test.yml new file mode 100644 index 000000000000..58415ff4dab3 --- /dev/null +++ b/.github/workflows/release-train-test.yml @@ -0,0 +1,84 @@ +# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit. +# To update it, modify .github/workflow-generator.yml as needed and re-run the generator. + +name: "Release Train – Test" +run-name: "${{ inputs.callback-ref }} – Test" +"on": + workflow_dispatch: + inputs: + callback: + description: "Repository to which a callback should be made upon completion" + required: true + type: "string" + callback-ref: + description: "Ref in the callback repository to which a callback should be made upon completion" + required: true + type: "string" + release-train-maven-repository-url: + description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train" + required: true + type: "string" +permissions: + contents: "read" +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" +jobs: + test-release: + name: "Test Release" + runs-on: "ubuntu22-2-8" + steps: + - name: "Prevent Re-runs" + id: "prevent-re-runs" + run: |- + if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then + echo "Re-runs are prohibited. Use the 'Release Train – Retry' workflow to retry test failures" + exit 1 + fi + - name: "Set up Java" + id: "set-up-java" + uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0 + with: + distribution: "liberica" + java-version: "25" + - name: "Check Out Code" + id: "check-out-code" + uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0 + - name: "Restore Build System Caches" + id: "restore-build-system-caches" + uses: "actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0 + with: + key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}" + path: |- + ~/.gradle/caches + ~/.gradle/wrapper + - name: "Test Release" + id: "test-release" + uses: "./.github/actions/release-train-test" + env: + COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}" + COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}" + COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}" + RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}" + RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}" + RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}" + - name: "Send Callback" + id: "send-callback" + if: "${{ !cancelled() }}" + env: + GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}" + run: |- + gh workflow run callback \ + --repo ${{ inputs.callback }} \ + --ref ${{ inputs.callback-ref }} \ + --field commit-hash=${{ steps.check-out-code.outputs.commit }} \ + --field release-branch=${{ github.ref_name }} \ + --field release-repository=${{ github.repository }} \ + --field result=${{ job.status == 'success' && 'tested' || 'test-failed' }} \ + --field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + - name: "Upload Build System Reports" + id: "upload-build-system-reports" + if: "${{ failure() }}" + uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1 + with: + name: "build-system-reports" + path: "**/build/reports" diff --git a/.github/workflows/update-antora-ui-spring.yml b/.github/workflows/update-antora-ui-spring.yml index ca6dd46679d0..5c8f411286ca 100644 --- a/.github/workflows/update-antora-ui-spring.yml +++ b/.github/workflows/update-antora-ui-spring.yml @@ -12,11 +12,12 @@ permissions: jobs: update-antora-ui-spring: - runs-on: ubuntu-latest name: Update on Supported Branches + if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ubuntu-latest strategy: matrix: - branch: [ '6.0.x', '6.1.x', 'main' ] + branch: [ '6.2.x', '7.0.x', 'main' ] steps: - uses: spring-io/spring-doc-actions/update-antora-spring-ui@5a57bcc6a0da2a1474136cf29571b277850432bc name: Update @@ -25,8 +26,9 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} antora-file-path: 'framework-docs/antora-playbook.yml' update-antora-ui-spring-docs-build: - runs-on: ubuntu-latest 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 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 000000000000..fb58b7e44e4e --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,88 @@ +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: + commercial-repository-password: + description: 'Password for authentication with the commercial repository' + required: false + commercial-repository-username: + description: 'Username for authentication with the commercial repository' + required: false + google-chat-webhook-url: + description: 'Google Chat Webhook URL' + required: true + opensource-repository-password: + description: 'Password for authentication with the open-source repository' + required: false + opensource-repository-username: + description: 'Username for authentication with the open-source repository' + required: false + token: + description: 'Token to use for authentication with GitHub' + required: true +permissions: + contents: read +jobs: + verify: + name: Verify + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} + steps: + - name: Check Out Release Verification Tests + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: 'v0.0.3' + repository: spring-projects/spring-framework-release-verification + token: ${{ secrets.token }} + - name: Check Out Send Notification Action + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: send-notification + sparse-checkout: .github/actions/send-notification + - name: Set Up Java + uses: actions/setup-java@v5 + with: + distribution: 'liberica' + java-version: 17 + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + with: + cache-provider: basic + 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_COMMERCIAL_REPOSITORY_PASSWORD: ${{ secrets.commercial-repository-password }} + RVT_COMMERCIAL_REPOSITORY_USERNAME: ${{ secrets.commercial-repository-username }} + RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.opensource-repository-password }} + RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.opensource-repository-username }} + RVT_RELEASE_TYPE: ${{ vars.COMMERCIAL && 'commercial' || '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@v7 + 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 549d5756e164..530ec7f8c9c3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ ivy-cache argfile* activemq-data/ classes/ +.cursor/ # Log files jxl.log @@ -38,11 +39,12 @@ bin .springBeans spring-*/src/main/java/META-INF/MANIFEST.MF -# IDEA artifacts and output dirs +# IntelliJ IDEA artifacts and output dirs *.iml *.ipr *.iws -.idea +.idea/* +!.idea/icon.svg out test-output atlassian-ide-plugin.xml @@ -53,3 +55,4 @@ atlassian-ide-plugin.xml 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/.sdkmanrc b/.sdkmanrc index d8db3808ef1e..2b4236b43e3c 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.11-librca +java=25-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..0b28403db7f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to the Spring Framework +# Contributing to the Spring Framework First off, thank you for taking the time to contribute! :+1: :tada: @@ -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. @@ -119,7 +120,7 @@ source code into your IDE. The wiki pages [Code Style](https://github.com/spring-projects/spring-framework/wiki/Code-Style) and [IntelliJ IDEA Editor Settings](https://github.com/spring-projects/spring-framework/wiki/IntelliJ-IDEA-Editor-Settings) -define the source file coding standards we use along with some IDEA editor settings we customize. +define the source file coding standards we use along with some IntelliJ editor settings we customize. ### Reference Docs diff --git a/README.md b/README.md index b9f9671488ef..edd478f0d829 100644 --- a/README.md +++ b/README.md @@ -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 @@ -27,7 +27,7 @@ See the [Build from Source](https://github.com/spring-projects/spring-framework/ ## Continuous Integration Builds -Information regarding CI builds can be found in the [Spring Framework Concourse pipeline](ci/README.adoc) documentation. +CI builds are defined with [GitHub Actions workflows](.github/workflows). ## Stay in Touch 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 fe60cafa1bb9..03ed9c649850 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,12 @@ plugins { - id 'io.freefair.aspectj' version '8.4' apply false + id 'io.freefair.aspectj' version '8.13.1' apply false // kotlinVersion is managed in gradle.properties id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false - id 'org.jetbrains.dokka' version '1.9.20' - id 'org.unbroken-dome.xjc' version '2.0.0' apply false - id 'com.github.ben-manes.versions' version '0.51.0' - id 'com.github.johnrengelman.shadow' version '8.1.1' apply false - id 'de.undercouch.download' version '5.4.0' + id 'org.jetbrains.dokka' + id 'com.github.bjornvester.xjc' version '1.8.2' apply false + id 'com.gradleup.shadow' version "9.2.2" 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 "3.1.0" apply false + id 'io.spring.nullability' version '0.0.14' apply false } ext { @@ -21,23 +18,8 @@ description = "Spring Framework" configure(allprojects) { project -> apply plugin: "org.springframework.build.localdev" + apply plugin: "org.springframework.build.repositories" 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" } - } - if (version.endsWith('-SNAPSHOT')) { - maven { url "https://repo.spring.io/snapshot" } - } - } configurations.all { resolutionStrategy { cacheChangingModulesFor 0, "seconds" @@ -64,52 +46,34 @@ 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 { - testImplementation("org.junit.jupiter:junit-jupiter-api") - testImplementation("org.junit.jupiter:junit-jupiter-params") - testImplementation("org.junit.platform:junit-platform-suite-api") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.junit.platform:junit-platform-suite") testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-junit-jupiter") - testImplementation("io.mockk:mockk") + testImplementation("io.mockk:mockk") { + exclude group: 'junit', module: 'junit' + } testImplementation("org.assertj:assertj-core") - // Pull in the latest JUnit 5 Launcher API to ensure proper support in IDEs. - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") 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.jboss.org/hibernate/orm/5.6/javadocs/", - "https://eclipse.dev/aspectj/doc/released/aspectj5rt-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://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.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://docs.oracle.com/en/java/javase/17/docs/api/", + //"https://jakarta.ee/specifications/platform/11/apidocs/", + "https://docs.hibernate.org/orm/7.2/javadocs/", + "https://www.quartz-scheduler.org/api/2.3.0/", + "https://hc.apache.org/httpcomponents-client-5.6.x/5.6/httpclient5/apidocs/", + "https://projectreactor.io/docs/core/release/api/", + "https://projectreactor.io/docs/test/release/api/", + "https://junit.org/junit4/javadoc/4.13.2/", + "https://docs.junit.org/6.1.2/api/", + "https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/", + "https://r2dbc.io/spec/1.0.0.RELEASE/api/", + "https://jspecify.dev/docs/api/" ] as String[] } diff --git a/buildSrc/README.md b/buildSrc/README.md index 9e35b5b766cf..3cf8ac690bd3 100644 --- a/buildSrc/README.md +++ b/buildSrc/README.md @@ -9,7 +9,18 @@ The `org.springframework.build.conventions` plugin applies all conventions to th * Configuring the Java compiler, see `JavaConventions` * Configuring the Kotlin compiler, see `KotlinConventions` -* Configuring testing in the build with `TestConventions` +* Configuring testing in the build with `TestConventions` +* Configuring the ArchUnit rules for the project, see `org.springframework.build.architecture.ArchitectureRules` + +This plugin also provides a DSL extension to optionally enable Java preview features for +compiling and testing sources in a module. This can be applied with the following in a +module build file: + +```groovy +springFramework { + enableJavaPreviewFeatures = true +} +``` ## Build Plugins @@ -22,6 +33,25 @@ but doesn't affect the classpath of dependent projects. This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly` configurations are preferred. +### MultiRelease Jar + +The `org.springframework.build.multiReleaseJar` plugin configures the project with MultiRelease JAR support. +It creates a new SourceSet and dedicated tasks for each Java variant considered. +This can be configured with the DSL, by setting a list of Java variants to configure: + +```groovy +plugins { + id 'org.springframework.build.multiReleaseJar' +} + +multiRelease { + releaseVersions 21, 24 +} +``` + +Note, Java classes will be compiled with the toolchain pre-configured by the project, assuming that its +Java language version is equal or higher than all variants we consider. Each compilation task will only +set the "-release" compilation option accordingly to produce the expected bytecode version. ### RuntimeHints Java Agent diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 19d41d438fe4..5f9621d86729 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -20,14 +20,24 @@ ext { dependencies { checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}" implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" - implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}" - implementation "org.gradle:test-retry-gradle-plugin:1.5.6" + implementation "org.jetbrains.dokka:dokka-gradle-plugin:2.2.0" + implementation "com.tngtech.archunit:archunit:1.4.1" + implementation "org.gradle:test-retry-gradle-plugin:1.6.2" implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}" implementation "io.spring.nohttp:nohttp-gradle:0.0.11" + + testImplementation("org.assertj:assertj-core:${assertjVersion}") + testImplementation(platform("org.junit:junit-bom:${junitVersion}")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } gradlePlugin { plugins { + architecturePlugin { + id = "org.springframework.architecture" + implementationClass = "org.springframework.build.architecture.ArchitecturePlugin" + } conventionsPlugin { id = "org.springframework.build.conventions" implementationClass = "org.springframework.build.ConventionsPlugin" @@ -36,6 +46,14 @@ gradlePlugin { id = "org.springframework.build.localdev" implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin" } + multiReleasePlugin { + id = "org.springframework.build.multiReleaseJar" + implementationClass = "org.springframework.build.multirelease.MultiReleaseJarPlugin" + } + repositoriesPlugin { + id = "org.springframework.build.repositories" + implementationClass = "org.springframework.build.RepositoriesPlugin" + } optionalDependenciesPlugin { id = "org.springframework.build.optional-dependencies" implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin" @@ -46,3 +64,9 @@ gradlePlugin { } } } + +test { + useJUnitPlatform() +} + +jar.dependsOn check diff --git a/buildSrc/config/checkstyle/checkstyle.xml b/buildSrc/config/checkstyle/checkstyle.xml index c63f232e1e70..78690ba2557d 100644 --- a/buildSrc/config/checkstyle/checkstyle.xml +++ b/buildSrc/config/checkstyle/checkstyle.xml @@ -1,27 +1,26 @@ - + - + - + - - - - + + + - \ No newline at end of file + diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties index e3edacf40c85..361684dbe054 100644 --- a/buildSrc/gradle.properties +++ b/buildSrc/gradle.properties @@ -1,2 +1,4 @@ org.gradle.caching=true -javaFormatVersion=0.0.41 +assertjVersion=3.27.3 +javaFormatVersion=0.0.43 +junitVersion=5.12.2 diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index 948219ac8032..e916cefbd74f 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.17.0"); + checkstyle.setToolVersion("13.10.0"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); @@ -63,8 +63,8 @@ private static void configureNoHttpPlugin(Project project) { project.getPlugins().apply(NoHttpPlugin.class); NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class); noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines")); - noHttp.getSource().exclude("**/test-output/**", "**/.settings/**", - "**/.classpath", "**/.project", "**/.gradle/**"); + 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..6dd95742465b 100644 --- a/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,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..741999760f23 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-present the original author 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; @@ -27,7 +26,6 @@ import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.compile.JavaCompile; import org.gradle.jvm.toolchain.JavaLanguageVersion; -import org.gradle.jvm.toolchain.JvmVendorSpec; /** * {@link Plugin} that applies conventions for compiling Java sources in Spring Framework. @@ -42,8 +40,21 @@ public class JavaConventions { private static final List TEST_COMPILER_ARGS; + /** + * The Java version we should use as the JVM baseline for building the project. + *

NOTE: If you update this value, you should also update the value used in + * the {@code javadoc} task in {@code framework-api.gradle}. + */ + private static final JavaLanguageVersion DEFAULT_LANGUAGE_VERSION = JavaLanguageVersion.of(25); + + /** + * 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 +62,74 @@ 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); + }); + } + + /** + * Configure the Toolchain support for the project. + * @param project the current project + */ + private static void applyToolchainConventions(Project project) { + project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> { + toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION); + }); } /** - * Applies the common Java compiler options for main sources, test fixture sources, and + * 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.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> { - toolchain.getVendor().set(JvmVendorSpec.BELLSOFT); - toolchain.getLanguageVersion().set(JavaLanguageVersion.of(17)); + 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..57dd0dcf0324 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-present the original author 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,33 +16,78 @@ 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.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.jetbrains.dokka.gradle.DokkaExtension; +import org.jetbrains.dokka.gradle.DokkaPlugin; +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 { void apply(Project project) { - project.getPlugins().withId("org.jetbrains.kotlin.jvm", - (plugin) -> project.getTasks().withType(KotlinCompile.class, this::configure)); + project.getPlugins().withId("org.jetbrains.kotlin.jvm", plugin -> { + project.getTasks().withType(KotlinCompile.class, this::configure); + if (project.getLayout().getProjectDirectory().dir("src/main/kotlin").getAsFile().exists()) { + project.getPlugins().apply(DokkaPlugin.class); + project.getExtensions().configure(DokkaExtension.class, dokka -> configure(project, dokka)); + project.project(":framework-api").getDependencies().add("dokka", 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_2); + options.getLanguageVersion().set(KotlinVersion.KOTLIN_2_2); + 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 + "-Xannotation-default-target=param-property" // Preferred behavior, default with Kotlin language version set to 2.4+, see https://youtrack.jetbrains.com/issue/KT-73255 + ); + }); + } + + private void configure(Project project, DokkaExtension dokka) { + dokka.getDokkaSourceSets().forEach(sourceSet -> { + sourceSet.getSourceRoots().setFrom(project.file("src/main/kotlin")); + sourceSet.getClasspath() + .from(project.getExtensions() + .getByType(SourceSetContainer.class) + .getByName(SourceSet.MAIN_SOURCE_SET_NAME) + .getOutput()); + var externalDocumentationLinks = sourceSet.getExternalDocumentationLinks(); + var springVersion = project.getVersion(); + externalDocumentationLinks.register("spring-framework", spec -> { + spec.url("https://docs.spring.io/spring-framework/docs/" + springVersion + "/javadoc-api/"); + spec.packageListUrl("https://docs.spring.io/spring-framework/docs/" + springVersion + "/javadoc-api/element-list"); + }); + externalDocumentationLinks.register("reactor-core", spec -> + spec.url("https://projectreactor.io/docs/core/release/api/")); + externalDocumentationLinks.register("reactive-streams", spec -> + spec.url("https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/")); + externalDocumentationLinks.register("kotlinx-coroutines", spec -> + spec.url("https://kotlinlang.org/api/kotlinx.coroutines/")); + externalDocumentationLinks.register("hamcrest", spec -> + spec.url("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/")); + externalDocumentationLinks.register("jakarta-servlet", spec -> { + spec.url("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/"); + spec.packageListUrl("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list"); + }); + externalDocumentationLinks.register("rsocket-core", spec -> + spec.url("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/")); + }); } } diff --git a/buildSrc/src/main/java/org/springframework/build/RepositoriesPlugin.java b/buildSrc/src/main/java/org/springframework/build/RepositoriesPlugin.java new file mode 100644 index 000000000000..6fb0fcf152f7 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/RepositoriesPlugin.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 org.gradle.api.Plugin; +import org.gradle.api.Project; + +/** + * Plugin that configures the OSS, commercial and release train repositories in the build. + * + * @author Brian Clozel + */ +public class RepositoriesPlugin implements Plugin { + + @Override + public void apply(Project project) { + configureOssRepositories(project); + configureCommercialRepositories(project); + configureReleaseTrainRepository(project); + } + + private void configureOssRepositories(Project project) { + project.getRepositories().mavenCentral(); + if (project.getVersion().toString().contains("-")) { + project.getRepositories().maven(repository -> { + repository.setName("spring-oss-milestone"); + repository.setUrl("https://repo.spring.io/milestone/"); + }); + } + if (project.getVersion().toString().endsWith("-SNAPSHOT")) { + project.getRepositories().maven(repository -> { + repository.setName("spring-oss-snapshot"); + repository.setUrl("https://repo.spring.io/snapshot/"); + }); + } + } + + private void configureCommercialRepositories(Project project) { + String releaseRepositoryUrl = getEnv("COMMERCIAL_RELEASE_REPO_URL"); + if (releaseRepositoryUrl != null) { + project.getRepositories().maven((repository) -> { + repository.setName("spring-commercial-release"); + repository.setUrl(releaseRepositoryUrl); + repository.credentials((creds) -> { + creds.setUsername(System.getenv("COMMERCIAL_REPO_USERNAME")); + creds.setPassword(System.getenv("COMMERCIAL_REPO_PASSWORD")); + }); + }); + } + String snapshotRepositoryUrl = getEnv("COMMERCIAL_SNAPSHOT_REPO_URL"); + if (snapshotRepositoryUrl != null && project.getVersion().toString().endsWith("-SNAPSHOT")) { + project.getRepositories().maven((repository) -> { + repository.setName("spring-commercial-snapshot"); + repository.setUrl(snapshotRepositoryUrl); + repository.credentials((creds) -> { + creds.setUsername(System.getenv("COMMERCIAL_REPO_USERNAME")); + creds.setPassword(System.getenv("COMMERCIAL_REPO_PASSWORD")); + }); + }); + } + } + + private void configureReleaseTrainRepository(Project project) { + String releaseTrainRepositoryUrl = getEnv("RELEASE_TRAIN_MAVEN_REPOSITORY_URL"); + if (releaseTrainRepositoryUrl != null) { + project.getRepositories().maven(repository -> { + repository.setName("spring-release-train"); + repository.setUrl(releaseTrainRepositoryUrl); + repository.credentials((creds) -> { + creds.setUsername(System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME")); + creds.setPassword(System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD")); + }); + }); + } + } + + /** + * Returns the environment variable's value, or {@code null} if it is unset or blank. + */ + private static String getEnv(String name) { + String value = System.getenv(name); + return (value != null && !value.isBlank()) ? value : null; + } +} 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..0d66aee84abd --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/SpringFrameworkExtension.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 2aaf8b39eb00..34b7eeff6c12 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-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,24 +16,31 @@ package org.springframework.build; -import java.util.Map; - 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.tasks.testing.Test; +import org.gradle.api.tasks.testing.TestFrameworkOptions; +import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions; import org.gradle.testretry.TestRetryPlugin; import org.gradle.testretry.TestRetryTaskExtension; +import java.util.Map; + /** * Conventions that are applied in the presence of the {@link JavaBasePlugin}. When the * plugin is applied: *

    *
  • The {@link TestRetryPlugin Test Retry} plugin is applied so that flaky tests * are retried 3 times when running on the CI server. + *
  • Common test properties are configured + *
  • The ByteBuddy Java agent is configured on test tasks. *
* * @author Brian Clozel * @author Andy Wilkinson + * @author Sam Brannen */ class TestConventions { @@ -42,6 +49,7 @@ void apply(Project project) { } private void configureTestConventions(Project project) { + configureByteBuddyAgent(project); project.getTasks().withType(Test.class, test -> { configureTests(project, test); @@ -50,21 +58,40 @@ private void configureTestConventions(Project project) { } private void configureTests(Project project, Test test) { - test.useJUnitPlatform(); + TestFrameworkOptions existingOptions = test.getOptions(); + test.useJUnitPlatform(options -> { + if (existingOptions instanceof JUnitPlatformOptions junitPlatformOptions) { + options.copyFrom(junitPlatformOptions); + } + }); test.include("**/*Tests.class", "**/*Test.class"); test.setSystemProperties(Map.of( "java.awt.headless", "true", "io.netty.leakDetection.level", "paranoid", - "io.netty5.leakDetectionLevel", "paranoid", - "io.netty5.leakDetection.targetRecords", "32", - "io.netty5.buffer.lifecycleTracingEnabled", "true" + "junit.platform.discovery.issue.severity.critical", "INFO" )); if (project.hasProperty("testGroups")) { - test.systemProperty("testGroups", project.getProperties().get("testGroups")); + test.systemProperty("testGroups", project.findProperty("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"); + "-Xshare:off" + ); + } + + private void configureByteBuddyAgent(Project project) { + if (project.hasProperty("byteBuddyVersion")) { + String byteBuddyVersion = (String) project.findProperty("byteBuddyVersion"); + Configuration byteBuddyAgentConfig = project.getConfigurations().create("byteBuddyAgentConfig"); + byteBuddyAgentConfig.setTransitive(false); + Dependency byteBuddyAgent = project.getDependencies().create("net.bytebuddy:byte-buddy-agent:" + byteBuddyVersion); + byteBuddyAgentConfig.getDependencies().add(byteBuddyAgent); + project.afterEvaluate(p -> { + p.getTasks().withType(Test.class, test -> test + .jvmArgs("-javaagent:" + byteBuddyAgentConfig.getAsPath())); + }); + } } private void configureTestRetryPlugin(Project project, Test test) { 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..223796142e24 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/architecture/ArchitectureCheck.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +/** + * {@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(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..7fbc8632742a --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/architecture/ArchitecturePlugin.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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..9e52b5f50e0d --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/architecture/ArchitectureRules.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 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/dev/LocalDevelopmentPlugin.java b/buildSrc/src/main/java/org/springframework/build/dev/LocalDevelopmentPlugin.java index 8c8a2fd4523c..c9c74933d6f7 100644 --- a/buildSrc/src/main/java/org/springframework/build/dev/LocalDevelopmentPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/dev/LocalDevelopmentPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentArgumentProvider.java b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentArgumentProvider.java index 2a7169fd885f..823bf7cb7fb2 100644 --- a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentArgumentProvider.java +++ b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentArgumentProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java index 6c7789cd02fd..816ed59ba652 100644 --- a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java +++ b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java index e0e303812368..04e63532b506 100644 --- a/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,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..cd506f9c2938 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseExtension.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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..91a92de0a2ae --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.JavaVersion; +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.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.AbstractArchiveTask; +import org.gradle.jvm.tasks.Jar; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +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 { + + public static String VALIDATE_JAR_TASK_NAME = "validateMultiReleaseJar"; + + @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); + + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_25)) { + TaskProvider validateJarTask = tasks.register(VALIDATE_JAR_TASK_NAME, MultiReleaseJarValidateTask.class, (task) -> { + task.getJar().set(tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile)); + task.getJavaLauncher().set(task.getJavaToolchainService().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(25)))); + }); + tasks.named("check", task -> task.dependsOn(validateJarTask)); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarValidateTask.java b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarValidateTask.java new file mode 100644 index 000000000000..fd1e49606499 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarValidateTask.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.JavaExec; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.jvm.toolchain.JavaToolchainService; + +import java.util.List; + +import javax.inject.Inject; + +@CacheableTask +public abstract class MultiReleaseJarValidateTask extends JavaExec { + + + public MultiReleaseJarValidateTask() { + getMainModule().set("jdk.jartool"); + getArgumentProviders().add(() -> List.of("--validate", "--file", getJar().get().getAsFile().getAbsolutePath())); + } + + @Inject + protected abstract JavaToolchainService getJavaToolchainService(); + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + public abstract RegularFileProperty getJar(); + +} diff --git a/buildSrc/src/main/java/org/springframework/build/optional/OptionalDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/build/optional/OptionalDependenciesPlugin.java index 89475866612d..a6549fb176e2 100644 --- a/buildSrc/src/main/java/org/springframework/build/optional/OptionalDependenciesPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/optional/OptionalDependenciesPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/buildSrc/src/main/java/org/springframework/build/shadow/ShadowSource.java b/buildSrc/src/main/java/org/springframework/build/shadow/ShadowSource.java index ed30b8609caa..6e3528f063bb 100644 --- a/buildSrc/src/main/java/org/springframework/build/shadow/ShadowSource.java +++ b/buildSrc/src/main/java/org/springframework/build/shadow/ShadowSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,7 +78,7 @@ public void relocate(String pattern, String destination) { } @OutputDirectory - DirectoryProperty getOutputDirectory() { + public DirectoryProperty getOutputDirectory() { return this.outputDirectory; } 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..24376a837cf2 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.gradle.testkit.runner.UnexpectedBuildFailure; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link MultiReleaseJarPlugin} + */ +public class MultiReleaseJarPluginTests { + + private File projectDir; + + private File buildFile; + + private File propertiesFile; + + @BeforeEach + void setup(@TempDir File projectDir) { + this.projectDir = projectDir; + this.buildFile = new File(this.projectDir, "build.gradle"); + this.propertiesFile = new File(this.projectDir, "gradle.properties"); + } + + @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")); + } + } + + @Test + @DisabledForJreRange(max = JRE.JAVA_24, disabledReason = "'jar --validate' is available as of Java 25") + void validateJar() throws IOException { + writeBuildFile(""" + plugins { + id 'java' + id 'org.springframework.build.multiReleaseJar' + } + version = '1.2.3' + tasks.withType(JavaCompile).configureEach { + options.release = 11 + } + multiRelease { releaseVersions 17 } + """); + writeGradleProperties(""" + org.gradle.jvmargs=-Duser.language=en + """); + writeClass("src/main/java17", "Main.java", """ + public class Main { + + public void method() {} + + } + """); + writeClass("src/main/java", "Main.java", """ + public class Main {} + """); + assertThatThrownBy(() ->runGradle("validateMultiReleaseJar")) + .isInstanceOf(UnexpectedBuildFailure.class) + .hasMessageContaining("entry: META-INF/versions/17/Main.class, contains a class with different api from earlier version"); + } + + private void writeBuildFile(String buildContent) throws IOException { + try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) { + out.print(buildContent); + } + } + + private void writeGradleProperties(String properties) throws IOException { + try (PrintWriter out = new PrintWriter(new FileWriter(this.propertiesFile))) { + out.print(properties); + } + } + + 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 387d0164b471..000000000000 --- a/ci/README.adoc +++ /dev/null @@ -1,59 +0,0 @@ -== Spring Framework Concourse pipeline - -NOTE: CI is being migrated to GitHub Actions. - -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.2.x[Spring Framework 6.2.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.2.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 c02c161cf073..000000000000 --- a/ci/images/ci-image/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM ubuntu:jammy-20240125 - -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 JDK23 /opt/openjdk/java23 - -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 06ef6a630d74..000000000000 --- a/ci/images/get-jdk-url.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e - -case "$1" in - java17) - echo "https://github.com/bell-sw/Liberica/releases/download/17.0.10%2B13/bellsoft-jdk17.0.10+13-linux-amd64.tar.gz" - ;; - java21) - echo "https://github.com/bell-sw/Liberica/releases/download/21.0.2%2B14/bellsoft-jdk21.0.2+14-linux-amd64.tar.gz" - ;; - java23) - echo "https://download.java.net/java/early_access/jdk23/17/GPL/openjdk-23-ea+17_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 e5109328674d..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 java23 -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 7572bf7c12de..000000000000 --- a/ci/parameters.yml +++ /dev/null @@ -1,10 +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.2.x" -build-name: "spring-framework" -pipeline-name: "spring-framework" -concourse-url: "https://ci.spring.io" diff --git a/ci/pipeline.yml b/ci/pipeline.yml deleted file mode 100644 index d7c908847090..000000000000 --- a/ci/pipeline.yml +++ /dev/null @@ -1,293 +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 - DEVELOCITY_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) - 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)) - 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 -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: artifactory-repo - type: artifactory-resource - icon: package-variant - source: - uri: ((artifactory-server)) - username: ((artifactory-username)) - password: ((artifactory-password)) - build_name: ((build-name)) -- 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: 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 - signing_key: ((signing-key)) - signing_passphrase: ((signing-passphrase)) - repo: libs-staging-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 - - 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 - - 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 - - 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: "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/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 < - javadoc moduleProject + rootProject.ext.moduleProjects.each { moduleProject -> + javadoc project(moduleProject.path) } } +def springAspectsOutput = project(":spring-aspects").sourceSets.main.output javadoc { + javadocTool.set(javaToolchains.javadocToolFor({ + languageVersion = JavaLanguageVersion.of(25) + })) + title = "${rootProject.description} ${version} API" + failOnError = true options { encoding = "UTF-8" memberLevel = JavadocMemberLevel.PROTECTED @@ -28,39 +35,40 @@ javadoc { header = rootProject.description use = true overview = project.relativePath("$rootProject.rootDir/framework-docs/src/docs/api/overview.html") - destinationDir = file("$project.docsDir/javadoc-api") + destinationDir = project.java.docsDir.dir("javadoc-api").get().asFile splitIndex = true links(rootProject.ext.javadocLinks) - addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint - addBooleanOption('Werror', true) // fail build on Javadoc warnings + // Check for 'syntax' and 'reference' during linting. + addBooleanOption('Xdoclint:syntax,reference', true) + // Change modularity mismatch from warn to info. + // See https://github.com/spring-projects/spring-framework/issues/27497 + addStringOption("-link-modularity-mismatch", "info") + // Fail build on Javadoc warnings. + addBooleanOption('Werror', true) } maxMemory = "1024m" doFirst { classpath += files( - // ensure the javadoc process can resolve types compiled from .aj sources - project(":spring-aspects").sourceSets.main.output + // ensure the javadoc process can resolve types compiled from .aj sources + springAspectsOutput ) - classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath }) + classpath += files(rootProject.ext.moduleProjects.collect { it.sourceSets.main.compileClasspath }) } } -/** - * Produce KDoc for all Spring Framework modules in "build/docs/kdoc" - */ -rootProject.tasks.dokkaHtmlMultiModule.configure { - dependsOn { - tasks.named("javadoc") +dokka { + moduleName = "spring-framework" + dokkaPublications.html { + outputDirectory = project.java.docsDir.dir("kdoc-api") + includes.from("$rootProject.rootDir/framework-docs/src/docs/api/dokka-overview.md") } - moduleName.set("spring-framework") - outputDirectory.set(file("$docsDir/kdoc-api")) - includes.from("$rootProject.rootDir/framework-docs/src/docs/api/dokka-overview.md") } /** * Zip all Java docs (javadoc & kdoc) into a single archive */ tasks.register('docsZip', Zip) { - dependsOn = ['javadoc', rootProject.tasks.dokkaHtmlMultiModule] + dependsOn = ['javadoc', 'dokkaGenerate'] group = "distribution" description = "Builds -${archiveClassifier} archive containing api and reference " + "for deployment at https://docs.spring.io/spring-framework/docs/." @@ -73,7 +81,7 @@ tasks.register('docsZip', Zip) { from(javadoc) { into "javadoc-api" } - from(rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) { + from(project.java.docsDir.dir("kdoc-api")) { into "kdoc-api" } } @@ -87,8 +95,8 @@ tasks.register('schemaZip', Zip) { archiveClassifier.set("schema") description = "Builds -${archiveClassifier} archive containing all " + "XSDs for deployment at https://springframework.org/schema." - duplicatesStrategy DuplicatesStrategy.EXCLUDE - moduleProjects.each { module -> + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + rootProject.ext.moduleProjects.each { module -> def Properties schemas = new Properties(); module.sourceSets.main.resources.find { diff --git a/framework-bom/framework-bom.gradle b/framework-bom/framework-bom.gradle index 840f20537fa2..da45bac746cf 100644 --- a/framework-bom/framework-bom.gradle +++ b/framework-bom/framework-bom.gradle @@ -8,7 +8,7 @@ group = "org.springframework" dependencies { constraints { parent.moduleProjects.sort { "$it.name" }.each { - api it + api project(it.path) } } } diff --git a/framework-docs/antora-playbook.yml b/framework-docs/antora-playbook.yml index 9fc2d93216e3..0182af768c60 100644 --- a/framework-docs/antora-playbook.yml +++ b/framework-docs/antora-playbook.yml @@ -13,8 +13,10 @@ content: - url: https://github.com/spring-projects/spring-framework # Refname matching: # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ - branches: ['main', '{6..9}.+({0..9}).x'] - tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})'] + # branches: We include snapshots for main, 6.2.x, and 7.0.x to 9.*.x. + branches: ['main', '6.2.x', '{7..9}.+({0..9}).x'] + # tags: include all releases from 6.2.0 to 9.*.*. + tags: ['v6.2.+({0..9})', 'v{7..9}.+({0..9}).+({0..9})?(-{RC,M}*)'] start_path: framework-docs asciidoc: extensions: @@ -36,4 +38,4 @@ runtime: failure_level: warn ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.26/ui-bundle.zip diff --git a/framework-docs/antora.yml b/framework-docs/antora.yml index 8cc3641a0d6c..c8c78a50db3d 100644 --- a/framework-docs/antora.yml +++ b/framework-docs/antora.yml @@ -6,7 +6,7 @@ nav: ext: collector: run: - command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :framework-docs:generateAntoraResources + command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g" :framework-docs:generateAntoraResources local: true scan: dir: ./build/generated-antora-resources @@ -42,7 +42,8 @@ asciidoc: spring-framework-reference: '{spring-framework-docs-root}/{spring-version}/reference' # # Other Spring portfolio projects - spring-boot-docs: '{docs-site}/spring-boot/docs/current/reference/html' + 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 @@ -72,6 +73,9 @@ asciidoc: 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' @@ -89,4 +93,5 @@ asciidoc: 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' \ No newline at end of file + 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 a150928d4ec0..a4cd3851a58b 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -1,3 +1,6 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + plugins { id 'kotlin' id 'io.spring.antora.generate-antora-yml' version '0.0.1' @@ -12,11 +15,15 @@ apply from: "${rootDir}/gradle/publications.gradle" antora { options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] environment = [ - 'BUILD_REFNAME': 'HEAD', - 'BUILD_VERSION': project.version, + 'BUILD_REFNAME': 'HEAD', + 'BUILD_VERSION': project.version, ] } +node { + version = '24.15.0' +} + tasks.named("generateAntoraYml") { asciidocAttributes = project.provider( { return ["spring-version": project.version ] @@ -37,35 +44,55 @@ javadoc { repositories { maven { - url "https://repo.spring.io/release" + url = "https://repo.spring.io/release" } } -dependencies { - api(project(":spring-context")) - api(project(":spring-jdbc")) - api(project(":spring-jms")) - api(project(":spring-web")) - api(project(":spring-webmvc")) - api(project(":spring-context-support")) - api(project(":spring-aspects")) - api(project(":spring-websocket")) - - api("org.jetbrains.kotlin:kotlin-stdlib") - api("jakarta.jms:jakarta.jms-api") - api("jakarta.servlet:jakarta.servlet-api") - api("org.apache.commons:commons-dbcp2:2.11.0") - api("com.mchange:c3p0:0.9.5.5") - api("com.fasterxml.jackson.core:jackson-databind") - api("com.fasterxml.jackson.module:jackson-module-parameter-names") - api("jakarta.validation:jakarta.validation-api") - api("org.aspectj:aspectjweaver") - api("io.projectreactor.netty:reactor-netty-http") - api("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") - api("javax.cache:cache-api") - api("jakarta.resource:jakarta.resource-api") - api("org.apache.activemq:activemq-ra:6.1.2") +// To avoid a redeclaration error with Kotlin compiler and set the JVM target +tasks.withType(KotlinCompilationTask.class).configureEach { + javaSources.from = [] + compilerOptions.jvmTarget = JvmTarget.JVM_17 + compilerOptions.freeCompilerArgs.addAll( + "-Xjdk-release=17", // Needed due to https://youtrack.jetbrains.com/issue/KT-49746 + "-Xannotation-default-target=param-property" // Upcoming default, see https://youtrack.jetbrains.com/issue/KT-73255 + ) +} +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.github.ben-manes.caffeine:caffeine") + implementation("com.mchange:c3p0:0.9.5.5") + implementation("com.oracle.database.jdbc:ojdbc11") + implementation("io.micrometer:context-propagation") + 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.apache.groovy:groovy-templates") + implementation("org.aspectj:aspectjweaver") implementation("org.assertj:assertj-core") + implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") + implementation("org.freemarker:freemarker") + implementation("org.jetbrains.kotlin:kotlin-stdlib") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + implementation("org.junit.jupiter:junit-jupiter-api") + implementation("tools.jackson.core:jackson-databind") + implementation("tools.jackson.dataformat:jackson-dataformat-xml") } diff --git a/framework-docs/modules/ROOT/assets/images/mvc-context-hierarchy.png b/framework-docs/modules/ROOT/assets/images/mvc-context-hierarchy.png index 9c4a950caadb..9ade68881de4 100644 Binary files a/framework-docs/modules/ROOT/assets/images/mvc-context-hierarchy.png and b/framework-docs/modules/ROOT/assets/images/mvc-context-hierarchy.png differ diff --git a/framework-docs/modules/ROOT/assets/images/mvc-context-hierarchy.svg b/framework-docs/modules/ROOT/assets/images/mvc-context-hierarchy.svg index 07148744b549..817a97ec2fe2 100644 --- a/framework-docs/modules/ROOT/assets/images/mvc-context-hierarchy.svg +++ b/framework-docs/modules/ROOT/assets/images/mvc-context-hierarchy.svg @@ -17,6 +17,7 @@ class="st5" id="svg5499" version="1.1" + font-family="Helvetica, Arial, sans-serif" inkscape:version="0.91 r13725" sodipodi:docname="mvc-splitted-contexts.svg" style="font-size:12px;overflow:visible;color-interpolation-filters:sRGB;fill:none;fill-rule:evenodd;stroke-linecap:square;stroke-miterlimit:3" @@ -36,7 +37,7 @@ inkscape:stockid="Arrow2Mend"><title id="title5505">Page-1DispatcherServlet + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;fill:#333333">DispatcherServlet Servlet WebApplicationContext + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;fill:#333333">Servlet WebApplicationContext (containing controllers, view resolvers,(containing controllers, view resolvers,and other web-related beans) Controllers + style="font-size:11.53426838px;fill:#333333">Controllers ViewResolver HandlerMapping + style="font-size:11.53426838px;fill:#333333">HandlerMapping Root WebApplicationContext (containing middle-tier services, datasources, etc.) @@ -541,43 +550,47 @@ height="36.72575" width="82.040657" id="rect6648" - style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" />Services Repositories + style="font-size:11.53426838px;fill:#333333">Repositories Delegates if no bean found \ No newline at end of file diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc index 867a49c5b46e..e14e12e840e5 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/beans-beans.adoc[] -*** xref:core/validation/conversion.adoc[] +*** xref:core/validation/data-binding.adoc[] +*** xref:core/validation/error-code-resolution.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[] @@ -98,98 +100,14 @@ *** xref:core/aop-api/autoproxy.adoc[] *** xref:core/aop-api/targetsource.adoc[] *** xref:core/aop-api/extensibility.adoc[] +** xref:core/resilience.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/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/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-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:data-access.adoc[] ** xref:data-access/transaction.adoc[] *** xref:data-access/transaction/motivation.adoc[] @@ -244,10 +162,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[] @@ -279,7 +197,10 @@ *** xref:web/webmvc-functional.adoc[] *** xref:web/webmvc/mvc-uri-building.adoc[] *** xref:web/webmvc/mvc-ann-async.adoc[] +*** xref:web/webmvc/mvc-range.adoc[] +*** xref:web/webmvc/mvc-data-binding.adoc[] *** xref:web/webmvc-cors.adoc[] +*** xref:web/webmvc-versioning.adoc[] *** xref:web/webmvc/mvc-ann-rest-exceptions.adoc[] *** xref:web/webmvc/mvc-security.adoc[] *** xref:web/webmvc/mvc-caching.adoc[] @@ -288,6 +209,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[] @@ -307,6 +229,7 @@ **** xref:web/webmvc/mvc-config/static-resources.adoc[] **** xref:web/webmvc/mvc-config/default-servlet-handler.adoc[] **** xref:web/webmvc/mvc-config/path-matching.adoc[] +**** xref:web/webmvc/mvc-config/api-version.adoc[] **** xref:web/webmvc/mvc-config/advanced-java.adoc[] **** xref:web/webmvc/mvc-config/advanced-xml.adoc[] *** xref:web/webmvc/mvc-http2.adoc[] @@ -339,7 +262,6 @@ **** xref:web/websocket/stomp/configuration-performance.adoc[] **** xref:web/websocket/stomp/stats.adoc[] **** xref:web/websocket/stomp/testing.adoc[] -** xref:web/integration.adoc[] * xref:web-reactive.adoc[] ** xref:web/webflux.adoc[] *** xref:web/webflux/new-framework.adoc[] @@ -373,7 +295,10 @@ **** xref:web/webflux/controller/ann-advice.adoc[] *** xref:web/webflux-functional.adoc[] *** xref:web/webflux/uri-building.adoc[] +*** xref:web/webflux/range.adoc[] +*** xref:web/webflux/data-binding.adoc[] *** xref:web/webflux-cors.adoc[] +*** xref:web/webflux-versioning.adoc[] *** xref:web/webflux/ann-rest-exceptions.adoc[] *** xref:web/webflux/security.adoc[] *** xref:web/webflux/caching.adoc[] @@ -390,11 +315,105 @@ *** xref:web/webflux-webclient/client-context.adoc[] *** xref:web/webflux-webclient/client-synchronous.adoc[] *** xref:web/webflux-webclient/client-testing.adoc[] -** xref:web/webflux-http-interface-client.adoc[] +** xref:web/webflux-http-service-client.adoc[] ** xref:web/webflux-websocket.adoc[] ** 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/javaconfig.adoc[] +**** xref:testing/testcontext-framework/ctx-management/xml.adoc[] +**** xref:testing/testcontext-framework/ctx-management/groovy.adoc[] +**** xref:testing/testcontext-framework/ctx-management/default-config.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/context-pausing.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/resttestclient.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[] @@ -423,8 +442,8 @@ *** xref:integration/cache/plug.adoc[] *** xref:integration/cache/specific-config.adoc[] ** xref:integration/observability.adoc[] +** xref:integration/aot-cache.adoc[] ** xref:integration/checkpoint-restore.adoc[] -** xref:integration/cds.adoc[] ** xref:integration/appendix.adoc[] * xref:languages.adoc[] ** xref:languages/kotlin.adoc[] @@ -433,14 +452,14 @@ *** xref:languages/kotlin/null-safety.adoc[] *** xref:languages/kotlin/classes-interfaces.adoc[] *** xref:languages/kotlin/annotations.adoc[] -*** xref:languages/kotlin/bean-definition-dsl.adoc[] +*** xref:languages/kotlin/bean-registration-dsl.adoc[] *** xref:languages/kotlin/web.adoc[] *** xref:languages/kotlin/coroutines.adoc[] *** xref:languages/kotlin/spring-projects-in.adoc[] *** xref:languages/kotlin/getting-started.adoc[] *** xref:languages/kotlin/resources.adoc[] ** xref:languages/groovy.adoc[] -** xref:languages/dynamic.adoc[] * xref:appendix.adoc[] -* {spring-framework-wiki}[Wiki] - +* {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 eb046cc27543..8c2cbd703ff8 100644 --- a/framework-docs/modules/ROOT/pages/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/appendix.adoc @@ -23,6 +23,12 @@ The following table lists all currently supported Spring properties. |=== | 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 @@ -32,7 +38,7 @@ for details. | `spring.beaninfo.ignore` | Instructs Spring to use the `Introspector.IGNORE_ALL_BEANINFO` mode when calling the JavaBeans `Introspector`. See -{spring-framework-api}++/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` @@ -49,15 +55,13 @@ 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 +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 +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. @@ -70,6 +74,11 @@ expressions used in XML bean definitions, `@Value`, etc. | The mode to use when compiling expressions for the xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring Expression Language]. +| `spring.expression.maxOperations` +| The default maximum number of operations permitted during +xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language] +expression evaluation. + | `spring.getenv.ignore` | Instructs Spring to ignore operating system environment variables if a Spring `Environment` property -- for example, a placeholder in a configuration String -- isn't @@ -77,6 +86,11 @@ resolvable otherwise. See {spring-framework-api}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`] for details. +| `spring.http.response.flush.enabled` +| Configures the Spring MVC `ServletServerHttpResponse` to allow flushing on the `OutputStream` +returned by `ServletServerHttpResponse#getBody()`. By default, such flush calls are ignored and +only `ServletServerHttpResponse#flush()` will actually flush the response to the network. + | `spring.jdbc.getParameterType.ignore` | Instructs Spring to ignore `java.sql.ParameterMetaData.getParameterType` completely. See the note in xref:data-access/jdbc/advanced.adoc#jdbc-batch-list[Batch Operations with a List of Objects]. @@ -88,11 +102,25 @@ the repeated JNDI lookup overhead. See {spring-framework-api}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`] for details. +| `spring.locking.strict` +| Instructs Spring to enforce strict locking during bean creation, rather than the mix of +strict and lenient locking that 6.2 applies by default. See +{spring-framework-api}++/beans/factory/support/DefaultListableBeanFactory.html#STRICT_LOCKING_PROPERTY_NAME++[`DefaultListableBeanFactory`] +for details. + | `spring.objenesis.ignore` | Instructs Spring to ignore Objenesis, not even attempting to use it. See {spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] for details. +| `spring.placeholder.escapeCharacter.default` +| The default escape character for property placeholder support. If not set, `'\'` will +be used. Can be set to a custom escape character or an empty string to disable support +for an escape character. The default escape character be explicitly overridden in +`PropertySourcesPlaceholderConfigurer` and subclasses of `AbstractPropertyResolver`. See +{spring-framework-api}++/core/env/AbstractPropertyResolver.html#DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME++[`AbstractPropertyResolver`] +for details. + | `spring.test.aot.processing.failOnError` | A boolean flag that controls whether errors encountered during AOT processing in the _Spring TestContext Framework_ should result in an exception that fails the overall process. @@ -106,11 +134,20 @@ on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#int | The maximum size of the context cache in the _Spring TestContext Framework_. See xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]. +| `spring.test.context.cache.pause` +| The pause mode for the context cache in the _Spring TestContext Framework_. See +xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[Context Pausing]. + | `spring.test.context.failure.threshold` | The failure threshold for errors encountered while attempting to load an `ApplicationContext` in the _Spring TestContext Framework_. See xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[Context Failure Threshold]. +| `spring.test.extension.context.scope` +| The default _extension context scope_ used by the `SpringExtension` in `@Nested` test +class hierarchies. See +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`]. + | `spring.test.enclosing.configuration` | The default _enclosing configuration inheritance mode_ to use if `@NestedTestConfiguration` is not present on a test class. See diff --git a/framework-docs/modules/ROOT/pages/core.adoc b/framework-docs/modules/ROOT/pages/core.adoc index 6803a4b8895f..bea4d6e6856b 100644 --- a/framework-docs/modules/ROOT/pages/core.adoc +++ b/framework-docs/modules/ROOT/pages/core.adoc @@ -17,14 +17,3 @@ is also provided. AOT processing can be used to optimize your application ahead-of-time. It is typically used for native image deployment using GraalVM. - - - - - - - - - - - diff --git a/framework-docs/modules/ROOT/pages/core/aop-api.adoc b/framework-docs/modules/ROOT/pages/core/aop-api.adoc index e159cf1867df..af94989a96b9 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api.adoc @@ -6,7 +6,3 @@ The previous chapter described the Spring's support for AOP with @AspectJ and sc aspect definitions. In this chapter, we discuss the lower-level Spring AOP APIs. For common applications, we recommend the use of Spring AOP with AspectJ pointcuts as described in the previous chapter. - - - - 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..81b997792161 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/advice.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/advice.adoc @@ -4,7 +4,6 @@ Now we can examine how Spring AOP handles advice. - [[aop-api-advice-lifecycle]] == Advice Lifecycles @@ -22,22 +21,20 @@ the advice adds state to the proxied object. You can use a mix of shared and per-instance advice in the same AOP proxy. - [[aop-api-advice-types]] == Advice Types in Spring Spring provides several advice types and is extensible to support arbitrary advice types. This section describes the basic concepts and standard advice types. - [[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 +46,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 +55,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 } } ---- @@ -101,11 +98,10 @@ you are likely to want to run the aspect in another AOP framework. Note that poi are not currently interoperable between frameworks, and the AOP Alliance does not 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 +118,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 +131,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 +149,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CountingBeforeAdvice : MethodBeforeAdvice { @@ -172,14 +164,13 @@ Kotlin:: 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 +180,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 +201,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class RemoteThrowsAdvice : ThrowsAdvice { @@ -220,15 +212,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 +232,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ServletThrowsAdviceWithArguments : ThrowsAdvice { @@ -259,7 +251,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 +267,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CombinedThrowsAdvice : ThrowsAdvice { @@ -300,11 +292,10 @@ exception that is incompatible with the target method's signature!_ 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 +317,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 +336,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CountingAfterReturningAdvice : AfterReturningAdvice { @@ -364,11 +355,10 @@ thrown up the interceptor chain instead of the return value. 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 +410,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 +421,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 +470,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 { @@ -504,13 +494,12 @@ Java:: } return super.invoke(invocation); } - } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class LockMixin : DelegatingIntroductionInterceptor(), Lockable { @@ -534,7 +523,6 @@ Kotlin:: } return super.invoke(invocation) } - } ---- ====== @@ -556,7 +544,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 +556,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) ---- @@ -585,8 +573,3 @@ We can apply this advisor programmatically by using the `Advised.addAdvisor()` m (the recommended way) in XML configuration, as any other advisor. All proxy creation choices discussed below, including "`auto proxy creators,`" correctly handle introductions and stateful mixins. - - - - - 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..57f3fd541122 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 @@ -142,7 +142,3 @@ case, the `Advised` `isFrozen()` method returns `true`, and any attempts to modi advice through addition or removal results in an `AopConfigException`. The ability to freeze the state of an advised object is useful in some cases (for example, to prevent calling code removing a security interceptor). - - - - diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/advisor.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/advisor.adoc index 2eac05210854..b65dbb2aa689 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/advisor.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/advisor.adoc @@ -14,7 +14,3 @@ It is possible to mix advisor and advice types in Spring in the same AOP proxy. example, you could use an interception around advice, throws advice, and before advice in one proxy configuration. Spring automatically creates the necessary interceptor chain. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/autoproxy.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/autoproxy.adoc index 60438bbc174d..5e439dc06986 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/autoproxy.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/autoproxy.adoc @@ -19,14 +19,12 @@ There are two ways to do this: auto-proxy creation driven by source-level metadata attributes. - [[aop-autoproxy-choices]] == Auto-proxy Bean Definitions This section covers the auto-proxy creators provided by the `org.springframework.aop.framework.autoproxy` package. - [[aop-api-autoproxy]] === `BeanNameAutoProxyCreator` @@ -61,7 +59,6 @@ automatically created by the `BeanNameAutoProxyCreator`. The same advice is appl to all matching beans. Note that, if advisors are used (rather than the interceptor in the preceding example), the pointcuts may apply differently to different beans. - [[aop-api-autoproxy-default]] === `DefaultAdvisorAutoProxyCreator` @@ -125,7 +122,3 @@ differently configured, AdvisorAutoProxyCreators in the same factory) and orderi Advisors can implement the `org.springframework.core.Ordered` interface to ensure correct ordering if this is an issue. The `TransactionAttributeSourceAdvisor` used in the preceding example has a configurable order value. The default setting is unordered. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/concise-proxy.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/concise-proxy.adoc index a0218c763080..f9d2f80de30e 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/concise-proxy.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/concise-proxy.adoc @@ -65,7 +65,3 @@ that, if you have a (parent) bean definition that you intend to use only as a te and this definition specifies a class, you must make sure to set the `abstract` attribute to `true`. Otherwise, the application context actually tries to pre-instantiate it. - - - - 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 ef1ab658f67b..701eef3f5f66 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/pfb.adoc @@ -14,7 +14,6 @@ the pointcuts, any advice that applies, and their ordering. However, there are s options that are preferable if you do not need such control. - [[aop-pfb-1]] == Basics @@ -32,7 +31,6 @@ application objects (besides the target, which should be available in any AOP framework), benefiting from all the pluggability provided by Dependency Injection. - [[aop-pfb-2]] == JavaBean Properties @@ -87,7 +85,6 @@ to be applied. You can find an example of using this feature in xref:core/aop-ap `false`. - [[aop-pfb-proxy-types]] == JDK- and CGLIB-based proxies @@ -137,7 +134,6 @@ interface that the target class implements to the `proxyInterfaces` property. Ho it is significantly less work and less prone to typographical errors. - [[aop-api-proxying-intf]] == Proxying Interfaces @@ -196,14 +192,14 @@ 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 ---- @@ -263,7 +259,6 @@ However, there are times when being able to obtain the un-advised target from th factory might actually be an advantage (for example, in certain test scenarios). - [[aop-api-proxying-class]] == Proxying Classes @@ -302,7 +297,6 @@ There is little performance difference between CGLIB proxies and dynamic proxies Performance should not be a decisive consideration in this case. - [[aop-global-advisors]] == Using "`Global`" Advisors @@ -325,7 +319,3 @@ two global advisors: ---- - - - - 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 ec60c9dc76ff..3399fc540f1b 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/pointcuts.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/pointcuts.adoc @@ -4,7 +4,6 @@ This section describes how Spring handles the crucial pointcut concept. - [[aop-api-concepts]] == Concepts @@ -69,7 +68,6 @@ TIP: If possible, try to make pointcuts static, allowing the AOP framework to ca results of pointcut evaluation when an AOP proxy is created. - [[aop-api-pointcut-ops]] == Operations on Pointcuts @@ -84,7 +82,6 @@ You can compose pointcuts by using the static methods in the expressions is usually a simpler approach. - [[aop-api-pointcuts-aspectj]] == AspectJ Expression Pointcuts @@ -95,14 +92,12 @@ uses an AspectJ-supplied library to parse an AspectJ pointcut expression string. See the xref:core/aop.adoc[previous chapter] for a discussion of supported AspectJ pointcut primitives. - [[aop-api-pointcuts-impls]] == Convenience Pointcut Implementations Spring provides several convenient pointcut implementations. You can use some of them directly; others are intended to be subclassed in application-specific pointcuts. - [[aop-api-pointcuts-static]] === Static Pointcuts @@ -146,7 +141,6 @@ You can use `RegexpMethodPointcutAdvisor` with any `Advice` type. An important type of static pointcut is a metadata-driven pointcut. This uses the values of metadata attributes (typically, source-level metadata). - [[aop-api-pointcuts-dynamic]] === Dynamic pointcuts @@ -172,7 +166,6 @@ other dynamic pointcuts. In Java 1.4, the cost is about five times that of other pointcuts. - [[aop-api-pointcuts-superclasses]] == Pointcut Superclasses @@ -187,7 +180,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 { @@ -199,7 +192,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class TestStaticPointcut : StaticMethodMatcherPointcut() { @@ -214,7 +207,6 @@ There are also superclasses for dynamic pointcuts. You can use custom pointcuts with any advice type. - [[aop-api-pointcuts-custom]] == Custom Pointcuts @@ -225,7 +217,3 @@ expression language, if you can. NOTE: Later versions of Spring may offer support for "`semantic pointcuts`" as offered by JAC -- for example, "`all methods that change instance variables in the target object.`" - - - - 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 5b891654715f..b69cac9f570a 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc @@ -22,7 +22,6 @@ rather than a singleton bean definition. This allows Spring to create a new targ instance when required. - [[aop-ts-swap]] == Hot-swappable Target Sources @@ -38,7 +37,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 +45,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) @@ -77,7 +76,6 @@ use a `TargetSource`), any `TargetSource` can be used in conjunction with arbitrary advice. - [[aop-ts-pool]] == Pooling Target Sources @@ -89,14 +87,12 @@ A crucial difference between Spring pooling and SLSB pooling is that Spring pool be applied to any POJO. As with Spring in general, this service can be applied in a non-invasive way. -Spring provides support for Commons Pool 2.2, which provides a +Spring provides support for Commons Pool 2, which provides a fairly efficient pooling implementation. You need the `commons-pool` Jar on your application's classpath to use this feature. You can also subclass `org.springframework.aop.target.AbstractPoolingTargetSource` to support any other pooling API. -NOTE: Commons Pool 1.5+ is also supported but is deprecated as of Spring Framework 4.2. - The following listing shows an example configuration: [source,xml,indent=0,subs="verbatim,quotes"] @@ -152,7 +148,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 +156,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) @@ -175,7 +171,6 @@ Simpler pooling is available by using auto-proxying. You can set the `TargetSour used by any auto-proxy creator. - [[aop-ts-prototype]] == Prototype Target Sources @@ -200,7 +195,6 @@ The only property is the name of the target bean. Inheritance is used in the source, the target bean must be a prototype bean definition. - [[aop-ts-threadlocal]] == `ThreadLocal` Target Sources @@ -226,7 +220,3 @@ always remember to correctly set and unset (where the latter involves a call to 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.adoc b/framework-docs/modules/ROOT/pages/core/aop.adoc index 3d86b381f32b..f037e736cb19 100644 --- a/framework-docs/modules/ROOT/pages/core/aop.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop.adoc @@ -32,7 +32,3 @@ AOP is used in the Spring Framework to: NOTE: If you are interested only in generic declarative services or other pre-packaged declarative middleware services such as pooling, you do not need to work directly with Spring AOP, and can skip most of this chapter. - - - - 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 28664394da12..b7fde6aa6a02 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) @@ -53,7 +53,3 @@ Kotlin:: ====== 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 4380293f2f1b..c2862871a443 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj.adoc @@ -11,6 +11,3 @@ there is no dependency on the AspectJ compiler or weaver. NOTE: Using the AspectJ compiler and weaver enables use of the full AspectJ language and is discussed in xref:core/aop/using-aspectj.adoc[Using AspectJ with Spring Applications]. - - - 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 55c3b9146650..e7765d50f4af 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 @@ -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) { @@ -727,7 +727,7 @@ of determining parameter names, an exception will be thrown. parameter names. This discoverer is only used if such APIs are present on the classpath. `StandardReflectionParameterNameDiscoverer` :: Uses the standard `java.lang.reflect.Parameter` API to determine parameter names. Requires that code be compiled with the `-parameters` - flag for `javac`. Recommended approach on Java 8+. + flag for `javac`. Recommended approach. `AspectJAdviceParameterNameDiscoverer` :: Deduces parameter names from the pointcut expression, `returning`, and `throwing` clauses. See the {spring-framework-api}/aop/aspectj/AspectJAdviceParameterNameDiscoverer.html[javadoc] @@ -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 @@ -937,5 +937,3 @@ reflection for javac-compiled classes). Consider collapsing such advice methods advice method per join point in each `@Aspect` class or refactor the pieces of advice into separate `@Aspect` classes that you can order at the aspect level via `Ordered` or `@Order`. ==== - - 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 4672c2d547b6..d526df0e2171 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 @@ -32,6 +32,3 @@ stereotype annotation that qualifies, as per the rules of Spring's component sca NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes it from auto-proxying. - - - 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 6fd5e242bf37..aca99711d352 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/ataspectj/example.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/ataspectj/example.adoc @@ -18,7 +18,8 @@ call `proceed` multiple times. The following listing shows the basic aspect impl include-code::./ConcurrentOperationExecutor[tag=snippet,indent=0] -`@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]. +`@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 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..008d735ed831 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 { @@ -60,6 +60,3 @@ Programming Guide for more information on `per` clauses. The `pertarget` instantiation model works in exactly the same way as `perthis`, but it creates one aspect instance for each unique target object at matched join points. - - - 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 1c84a3beb713..ecb8589c8674 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,17 +63,15 @@ 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") ---- ====== - - 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 3b1ef29d767a..c8e4f00cc4c1 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 @@ -104,7 +104,7 @@ Note that pointcut definitions are generally matched against any intercepted met If a pointcut is strictly meant to be public-only, even in a CGLIB proxy scenario with potential non-public interactions through proxies, it needs to be defined accordingly. -If your interception needs include method calls or even constructors within the target +If your interception needs to include method calls or even constructors within the target class, consider the use of Spring-driven xref:core/aop/using-aspectj.adoc#aop-aj-ltw[native AspectJ weaving] instead of Spring's proxy-based AOP framework. This constitutes a different mode of AOP usage with different characteristics, so be sure to make yourself familiar with weaving @@ -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 @@ -581,6 +581,3 @@ performance (time and memory used), due to extra processing and analysis. Scopin designators are very fast to match, and using them means AspectJ can very quickly dismiss groups of join points that should not be further processed. A good pointcut should always include one if possible. - - - diff --git a/framework-docs/modules/ROOT/pages/core/aop/choosing.adoc b/framework-docs/modules/ROOT/pages/core/aop/choosing.adoc index d5432fce394a..5b437858d6d5 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/choosing.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/choosing.adoc @@ -8,7 +8,6 @@ decisions are influenced by a number of factors including application requiremen development tools, and team familiarity with AOP. - [[aop-spring-or-aspectj]] == Spring AOP or Full AspectJ? @@ -31,7 +30,6 @@ the @AspectJ style, sticking with regular Java compilation in your IDE, and addi an aspect weaving phase to your build script. - [[aop-ataspectj-or-xml]] == @AspectJ or XML for Spring AOP? @@ -59,7 +57,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 +71,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Pointcut("execution(* get*())") fun propertyAccess() {} @@ -107,7 +105,3 @@ Spring AOP and by AspectJ. So, if you later decide you need the capabilities of to implement additional requirements, you can easily migrate to a classic AspectJ setup. In general, the Spring team prefers the @AspectJ style for custom aspects beyond simple configuration of enterprise services. - - - - 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..656fb0fc2a37 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 @@ -73,7 +73,3 @@ it from older technologies offering only interception. Pointcuts enable advice t targeted independently of the object-oriented hierarchy. For example, you can apply an around advice providing declarative transaction management to a set of methods that span multiple objects (such as all business operations in the service layer). - - - - diff --git a/framework-docs/modules/ROOT/pages/core/aop/introduction-proxies.adoc b/framework-docs/modules/ROOT/pages/core/aop/introduction-proxies.adoc index d13db9196853..59eedb42b16b 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/introduction-proxies.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/introduction-proxies.adoc @@ -14,9 +14,5 @@ need to advise a method that is not declared on an interface or where you need t pass a proxied object to a method as a concrete type. It is important to grasp the fact that Spring AOP is proxy-based. See -xref:core/aop/proxying.adoc#aop-understanding-aop-proxies[Understanding AOP Proxies] for a thorough examination of exactly what this -implementation detail actually means. - - - - +xref:core/aop/proxying.adoc#aop-understanding-aop-proxies[Understanding AOP Proxies] +for a thorough examination of exactly what this implementation detail actually means. diff --git a/framework-docs/modules/ROOT/pages/core/aop/introduction-spring-defn.adoc b/framework-docs/modules/ROOT/pages/core/aop/introduction-spring-defn.adoc index 84ef5d6e5829..a59efbed2d32 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/introduction-spring-defn.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/introduction-spring-defn.adoc @@ -52,10 +52,6 @@ configuration-style approach. The fact that this chapter chooses to introduce th @AspectJ-style approach first should not be taken as an indication that the Spring team favors the @AspectJ annotation-style approach over the Spring XML configuration-style. -See xref:core/aop/choosing.adoc[Choosing which AOP Declaration Style to Use] for a more complete discussion of the advantages and disadvantages of -each style. +See xref:core/aop/choosing.adoc[Choosing which AOP Declaration Style to Use] for a more +complete discussion of the advantages and disadvantages of each style. ==== - - - - diff --git a/framework-docs/modules/ROOT/pages/core/aop/mixing-styles.adoc b/framework-docs/modules/ROOT/pages/core/aop/mixing-styles.adoc index 84bb7c41ada0..81e3de6d9e4a 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/mixing-styles.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/mixing-styles.adoc @@ -6,7 +6,3 @@ It is perfectly possible to mix @AspectJ style aspects by using the auto-proxyin schema-defined `` aspects, `` declared advisors, and even proxies and interceptors in other styles in the same configuration. All of these are implemented by using the same underlying support mechanism and can co-exist without any difficulty. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc b/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc index 44a76320f6f6..429e5d6e7ef1 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc @@ -6,24 +6,32 @@ 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. -* Your CGLIB proxy usage may face limitations with the JDK 9+ platform 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 +* `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. + +[[aop-forcing-proxy-types]] +== Forcing Specific AOP Proxy Types + To force the use of CGLIB proxies, set the value of the `proxy-target-class` attribute of the `` element to true, as follows: @@ -56,6 +64,23 @@ To be clear, using `proxy-target-class="true"` on ``, proxies _for all three of them_. ==== +`@EnableAspectJAutoProxy`, `@EnableTransactionManagement` and related configuration +annotations offer a corresponding `proxyTargetClass` attribute. These are collapsed +into a single unified auto-proxy creator too, effectively applying the _strongest_ +proxy settings at runtime. As of 7.0, this applies to individual proxy processors +as well, for example `@EnableAsync`, consistently participating in unified global +default settings for all auto-proxying attempts in a given application. + +The global default proxy type may differ between setups. While the core framework +suggests interface-based proxies by default, Spring Boot may - depending on +configuration properties - enable class-based proxies by default. + +As of 7.0, forcing a specific proxy type for individual beans is possible through +the `@Proxyable` annotation on a given `@Bean` method or `@Component` class, with +`@Proxyable(INTERFACES)` or `@Proxyable(TARGET_CLASS)` overriding any globally +configured default. For very specific purposes, you may even specify the proxy +interface(s) to use through `@Proxyable(interfaces=...)`, limiting the exposure +to selected interfaces rather than all interfaces that the target bean implements. [[aop-understanding-aop-proxies]] @@ -65,15 +90,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 { @@ -90,7 +114,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- class SimplePojo : Pojo { @@ -115,7 +139,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 { @@ -129,7 +153,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun main() { val pojo = SimplePojo() @@ -148,7 +172,7 @@ image::aop-proxy-call.png[] ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- public class Main { @@ -166,7 +190,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun main() { val factory = ProxyFactory(SimplePojo()) @@ -187,26 +211,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(); } @@ -218,12 +251,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() } @@ -234,16 +267,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 { @@ -262,7 +295,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun main() { val factory = ProxyFactory(SimplePojo()) @@ -277,9 +310,5 @@ 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/schema.adoc b/framework-docs/modules/ROOT/pages/core/aop/schema.adoc index c51ad3e976fd..c8a7747975e7 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/schema.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/schema.adoc @@ -26,7 +26,6 @@ use either only the `` style or only the `AutoProxyCreator` style an never mix them. - [[aop-schema-declaring-an-aspect]] == Declaring an Aspect @@ -54,7 +53,6 @@ The bean that backs the aspect (`aBean` in this case) can of course be configure dependency injected just like any other Spring bean. - [[aop-schema-pointcuts]] == Declaring a Pointcut @@ -136,7 +134,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 +143,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun monitor(service: Any) { // ... @@ -177,9 +175,7 @@ follows: Note that pointcuts defined in this way are referred to by their XML `id` and cannot be used as named pointcuts to form composite pointcuts. The named pointcut support in the -schema-based definition style is thus more limited than that offered by the @AspectJ -style. - +schema-based definition style is thus more limited than that offered by the @AspectJ style. [[aop-schema-advice]] @@ -188,7 +184,6 @@ style. The schema-based AOP support uses the same five kinds of advice as the @AspectJ style, and they have exactly the same semantics. - [[aop-schema-advice-before]] === Before Advice @@ -237,7 +232,6 @@ that contains the advice. Before a data access operation is performed (a method join point matched by the pointcut expression), the `doAccessCheck` method on the aspect bean is invoked. - [[aop-schema-advice-after-returning]] === After Returning Advice @@ -282,20 +276,19 @@ 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) {... ---- ====== - [[aop-schema-advice-after-throwing]] === After Throwing Advice @@ -340,20 +333,19 @@ 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) {... ---- ====== - [[aop-schema-advice-after-finally]] === After (Finally) Advice @@ -372,7 +364,6 @@ You can declare it by using the `after` element, as the following example shows: ---- - [[aop-schema-advice-around]] === Around Advice @@ -421,7 +412,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 +424,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? { // start stopwatch @@ -444,17 +435,18 @@ Kotlin:: ---- ====== - [[aop-schema-params]] === Advice Parameters The schema-based declaration style supports fully typed advice in the same way as described for the @AspectJ support -- by matching pointcut parameters by name against -advice method parameters. See xref:core/aop/ataspectj/advice.adoc#aop-ataspectj-advice-params[Advice Parameters] for details. If you wish -to explicitly specify argument names for the advice methods (not relying on the +advice method parameters. See +xref:core/aop/ataspectj/advice.adoc#aop-ataspectj-advice-params[Advice Parameters] for details. +If you wish to explicitly specify argument names for the advice methods (not relying on the detection strategies previously described), you can do so by using the `arg-names` attribute of the advice element, which is treated in the same manner as the `argNames` -attribute in an advice annotation (as described in xref:core/aop/ataspectj/advice.adoc#aop-ataspectj-advice-params-names[Determining Argument Names]). +attribute in an advice annotation (as described in +xref:core/aop/ataspectj/advice.adoc#aop-ataspectj-advice-params-names[Determining Argument Names]). The following example shows how to specify an argument name in XML: [source,xml,indent=0,subs="verbatim"] @@ -464,7 +456,8 @@ The following example shows how to specify an argument name in XML: method="audit" arg-names="auditable" /> ---- -<1> References the `publicMethod` named pointcut defined in xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[Combining Pointcut Expressions]. +<1> References the `publicMethod` named pointcut defined in +xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[Combining Pointcut Expressions]. The `arg-names` attribute accepts a comma-delimited list of parameter names. @@ -475,7 +468,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 +487,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 +514,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 +538,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 +603,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 +617,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun main() { val ctx = ClassPathXmlApplicationContext("beans.xml") @@ -645,15 +638,15 @@ ms % Task name 00000 ? execution(getFoo) ---- - [[aop-ordering]] === Advice Ordering When multiple pieces of advice need to run at the same join point (executing method) -the ordering rules are as described in xref:core/aop/ataspectj/advice.adoc#aop-ataspectj-advice-ordering[Advice Ordering]. The precedence -between aspects is determined via the `order` attribute in the `` element or -by either adding the `@Order` annotation to the bean that backs the aspect or by having -the bean implement the `Ordered` interface. +the ordering rules are as described in +xref:core/aop/ataspectj/advice.adoc#aop-ataspectj-advice-ordering[Advice Ordering]. The +precedence between aspects is determined via the `order` attribute in the `` +element or by either adding the `@Order` annotation to the bean that backs the aspect +or by having the bean implement the `Ordered` interface. [NOTE] ==== @@ -676,7 +669,6 @@ at the aspect level. ==== - [[aop-schema-introductions]] == Introductions @@ -714,7 +706,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 +715,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- fun recordUsage(usageTracked: UsageTracked) { usageTracked.incrementUseCount() @@ -742,21 +734,20 @@ 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) ---- ====== - [[aop-schema-instantiation-models]] == Aspect Instantiation Models @@ -764,7 +755,6 @@ The only supported instantiation model for schema-defined aspects is the singlet model. Other instantiation models may be supported in future releases. - [[aop-schema-advisors]] == Advisors @@ -772,7 +762,8 @@ The concept of "advisors" comes from the AOP support defined in Spring and does not have a direct equivalent in AspectJ. An advisor is like a small self-contained aspect that has a single piece of advice. The advice itself is represented by a bean and must implement one of the advice interfaces described in -xref:core/aop-api/advice.adoc#aop-api-advice-types[Advice Types in Spring]. Advisors can take advantage of AspectJ pointcut expressions. +xref:core/aop-api/advice.adoc#aop-api-advice-types[Advice Types in Spring]. +Advisors can take advantage of AspectJ pointcut expressions. Spring supports the advisor concept with the `` element. You most commonly see it used in conjunction with transactional advice, which also has its own @@ -805,7 +796,6 @@ To define the precedence of an advisor so that the advice can participate in ord use the `order` attribute to define the `Ordered` value of the advisor. - [[aop-schema-example]] == An AOP Schema Example @@ -829,7 +819,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 +859,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- class ConcurrentOperationExecutor : Ordered { @@ -953,7 +943,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 +953,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Retention(AnnotationRetention.RUNTIME) // marker annotation @@ -981,7 +971,3 @@ pointcut expression so that only `@Idempotent` operations match, as follows: expression="execution(* com.xyz.service.*.*(..)) and @annotation(com.xyz.service.Idempotent)"/> ---- - - - - 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 c9176447ebcd..b03a392596f5 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/using-aspectj.adoc @@ -8,12 +8,14 @@ alone. Spring ships with a small AspectJ aspect library, which is available stand-alone in your distribution as `spring-aspects.jar`. You need to add this to your classpath in order -to use the aspects in it. xref:core/aop/using-aspectj.adoc#aop-atconfigurable[Using AspectJ to Dependency Inject Domain Objects with Spring] and xref:core/aop/using-aspectj.adoc#aop-ajlib-other[Other Spring aspects for AspectJ] discuss the -content of this library and how you can use it. xref:core/aop/using-aspectj.adoc#aop-aj-configure[Configuring AspectJ Aspects by Using Spring IoC] discusses how to -dependency inject AspectJ aspects that are woven using the AspectJ compiler. Finally, -xref:core/aop/using-aspectj.adoc#aop-aj-ltw[Load-time Weaving with AspectJ in the Spring Framework] provides an introduction to load-time weaving for Spring applications -that use AspectJ. - +to use the aspects in it. +xref:core/aop/using-aspectj.adoc#aop-atconfigurable[Using AspectJ to Dependency Inject Domain Objects with Spring] +and xref:core/aop/using-aspectj.adoc#aop-ajlib-other[Other Spring aspects for AspectJ] +discuss the content of this library and how you can use it. +xref:core/aop/using-aspectj.adoc#aop-aj-configure[Configuring AspectJ Aspects by Using Spring IoC] +discusses how to dependency inject AspectJ aspects that are woven using the AspectJ compiler. Finally, +xref:core/aop/using-aspectj.adoc#aop-aj-ltw[Load-time Weaving with AspectJ in the Spring Framework] +provides an introduction to load-time weaving for Spring applications that use AspectJ. [[aop-atconfigurable]] @@ -36,7 +38,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 +52,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 @@ -84,7 +86,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 +100,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 @@ -153,14 +155,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) ---- @@ -206,7 +208,6 @@ not use `@Configurable` on bean classes that are registered as regular Spring be with the container. Doing so results in double initialization, once through the container and once through the aspect. - [[aop-configurable-testing]] === Unit Testing `@Configurable` Objects @@ -219,7 +220,6 @@ you can still unit test outside of the container as normal, but you see a warnin message each time that you construct a `@Configurable` object indicating that it has not been configured by Spring. - [[aop-configurable-container]] === Working with Multiple Application Contexts @@ -249,7 +249,6 @@ is added only to the container-wide classpath (and hence loaded by the shared pa not what you want). - [[aop-ajlib-other]] == Other Spring aspects for AspectJ @@ -302,7 +301,6 @@ fully qualified class names: ---- - [[aop-aj-configure]] == Configuring AspectJ Aspects by Using Spring IoC @@ -357,7 +355,6 @@ results in the creation of Spring AOP proxies. The @AspectJ style of aspect declaration is being used here, but the AspectJ runtime is not involved. - [[aop-aj-ltw]] == Load-time Weaving with AspectJ in the Spring Framework @@ -389,8 +386,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 -{spring-github-org}/spring-petclinic[Petclinic sample application]. - +{petclinic-github-org}/spring-framework-petclinic[Petclinic sample application based on Spring Framework]. [[aop-aj-ltw-first-example]] === A First Example @@ -413,7 +409,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; @@ -446,7 +442,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz @@ -544,7 +540,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; @@ -566,7 +562,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz @@ -625,7 +621,7 @@ result: ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary",chomp="-packages"] +[source,java,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz; @@ -647,7 +643,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary",chomp="-packages"] +[source,kotlin,indent=0,subs="verbatim",chomp="-packages"] ---- package com.xyz @@ -677,7 +673,6 @@ nice example of a development-time aspect that developers can use during develop and then easily exclude from builds of the application being deployed into UAT or production. - [[aop-aj-ltw-the-aspects]] === Aspects @@ -686,7 +681,6 @@ either the AspectJ language itself, or you can write your aspects in the @Aspect Your aspects are then both valid AspectJ and Spring AOP aspects. Furthermore, the compiled aspect classes need to be available on the classpath. - [[aop-aj-ltw-aop_dot_xml]] === `META-INF/aop.xml` @@ -716,7 +710,6 @@ The structure and contents of this file is detailed in the LTW part of the {aspectj-docs-devguide}/ltw-configuration.html[AspectJ reference documentation]. Because the `aop.xml` file is 100% AspectJ, we do not describe it further here. - [[aop-aj-ltw-libraries]] === Required libraries (JARS) @@ -731,7 +724,6 @@ If you use the xref:core/aop/using-aspectj.adoc#aop-aj-ltw-environments-generic[ * `spring-instrument.jar` - [[aop-aj-ltw-spring]] === Spring Configuration @@ -831,7 +823,6 @@ possible values: then AspectJ weaving is on. Otherwise, it is off. This is the default value. |=== - [[aop-aj-ltw-environments]] === Environment-specific Configuration @@ -880,7 +871,3 @@ Note that this requires modification of the JVM launch script, which may prevent from using this in application server environments (depending on your server and your operation policies). That said, for one-app-per-JVM deployments such as standalone Spring Boot applications, you typically control the entire JVM setup in any case. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/aot.adoc b/framework-docs/modules/ROOT/pages/core/aot.adoc index 5a15c6e23af0..b16ac2728da9 100644 --- a/framework-docs/modules/ROOT/pages/core/aot.adoc +++ b/framework-docs/modules/ROOT/pages/core/aot.adoc @@ -5,6 +5,7 @@ This chapter covers Spring's Ahead of Time (AOT) optimizations. For AOT support specific to integration tests, see xref:testing/testcontext-framework/aot.adoc[Ahead of Time Support for Tests]. + [[aot.introduction]] == Introduction to Ahead of Time Optimizations @@ -17,9 +18,9 @@ Applying such optimizations early implies the following restrictions: * The beans defined in your application cannot change at runtime, meaning: ** `@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. +* 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. +`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. @@ -35,8 +36,9 @@ A Spring AOT processed application typically generates: 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` 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`]: @@ -51,6 +53,7 @@ The `RuntimeHints` instance can also be used to generate the relevant GraalVM na Those steps are covered in greater detail in the sections below. + [[aot.refresh]] == Refresh for AOT Processing @@ -88,6 +91,7 @@ This makes sure to create any proxy that will be required at runtime. Once this part completes, the `BeanFactory` contains the bean definitions that are necessary for the application to run. It does not trigger bean instantiation but allows the AOT engine to inspect the beans that will be created at runtime. + [[aot.bean-factory-initialization-contributions]] == Bean Factory Initialization AOT Contributions @@ -106,11 +110,10 @@ Consequently, such a bean is automatically excluded from the AOT-optimized conte [NOTE] ==== If a bean implements the `BeanFactoryInitializationAotProcessor` interface, the bean and **all** of its dependencies will be initialized during AOT processing. -We generally recommend that this interface is only implemented by infrastructure beans such as `BeanFactoryPostProcessor` which have limited dependencies and are already initialized early in the bean factory lifecycle. +We generally recommend that this interface is only implemented by infrastructure beans, such as a `BeanFactoryPostProcessor`, which have limited dependencies and are already initialized early in the bean factory lifecycle. If such a bean is registered using an `@Bean` factory method, ensure the method is `static` so that its enclosing `@Configuration` class does not have to be initialized. ==== - [[aot.bean-registration-contributions]] === Bean Registration AOT Contributions @@ -127,7 +130,7 @@ Typically used when the bean definition needs to be tuned for specific features [NOTE] ==== If a bean implements the `BeanRegistrationAotProcessor` interface, the bean and **all** of its dependencies will be initialized during AOT processing. -We generally recommend that this interface is only implemented by infrastructure beans such as `BeanFactoryPostProcessor` which have limited dependencies and are already initialized early in the bean factory lifecycle. +We generally recommend that this interface is only implemented by infrastructure beans, such as a `BeanFactoryPostProcessor`, which have limited dependencies and are already initialized early in the bean factory lifecycle. If such a bean is registered using an `@Bean` factory method, ensure the method is `static` so that its enclosing `@Configuration` class does not have to be initialized. ==== @@ -140,7 +143,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 { @@ -155,7 +158,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration(proxyBeanMethods = false) class DataSourceConfiguration { @@ -176,7 +179,7 @@ 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} @@ -219,21 +222,23 @@ NOTE: The exact code generated may differ depending on the exact nature of your 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. +The generated code above creates bean definitions equivalent to the `@Configuration` class, but in a direct way and without the use of reflection at all if 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 +== 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 +is automatically enabled when running within a native image. However it is also possible to use AOT 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. +NOTE: When AOT optimizations are included, some decisions that have been made 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 @@ -244,7 +249,7 @@ 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 +=== 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 @@ -266,19 +271,19 @@ notion of a classpath. For cases like this, it is crucial that the scanning happ 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. -For `@Configuration` classes, make sure that the return type of the factory `@Bean` method is as precise as possible. +For `@Configuration` classes, make sure that the return type of an `@Bean` factory method is as precise as possible. Consider the following example: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration(proxyBeanMethods = false) public class UserConfiguration { @@ -290,19 +295,32 @@ 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`. -None of the usual post-processing will take `MyImplementation` into account. -For instance, if there is an annotated handler method on `MyImplementation` that the context should register, it won’t be detected upfront. +In the example above, the declared type for the `myInterface` bean is `MyInterface`. +During AOT processing, none of the usual post-processing will take `MyImplementation` into account. +For instance, if there is an annotated handler method on `MyImplementation` that the context should register, it will not be detected during AOT processing. -The example above should be rewritten as follows: +The example above should therefore be rewritten as follows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration(proxyBeanMethods = false) public class UserConfiguration { @@ -314,6 +332,19 @@ 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. @@ -322,28 +353,55 @@ If you are registering bean definitions programmatically, consider using `RootBe === 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. +However, relying on that 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 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 `org.springframework.aot.generate.ValueCodeGenerator$Delegate` as the key. + [[aot.bestpractices.custom-arguments]] -=== Avoid Creating Bean with 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: +Spring AOT detects what needs to be done to create a bean and translates that into generated code that uses 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] which can lead 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. +. Bypassing 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 `FactoryBean` should be used with care as it introduces an intermediate layer in terms of bean type resolution that may not be conceptually necessary. -As a rule of thumb, if the `FactoryBean` instance does not hold long-term state and is not needed at a later point in time at runtime, it should be replaced by a regular factory method, possibly with a `FactoryBean` adapter layer on top (for declarative configuration purposes). +As a rule of thumb, if a `FactoryBean` instance does not hold long-term state and is not needed at a later point at runtime, it should be replaced by a regular `@Bean` factory method, possibly with a `FactoryBean` adapter layer on top (for declarative configuration purposes). If your `FactoryBean` implementation does not resolve the object type (i.e. `T`), extra care is necessary. Consider the following example: @@ -352,12 +410,21 @@ 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 { + // ... + } +---- ====== A concrete client declaration should provide a resolved generic for the client, as shown in the following example: @@ -366,7 +433,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 { @@ -378,9 +445,22 @@ 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: +If a `FactoryBean` bean definition is registered programmatically, make sure to follow these steps: 1. Use `RootBeanDefinition`. 2. Set the `beanClass` to the `FactoryBean` class so that AOT knows that it is an intermediate layer. @@ -392,12 +472,22 @@ 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); + 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) ---- ====== @@ -410,7 +500,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) { @@ -420,16 +510,29 @@ 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 +To ensure that entity scanning occurs ahead of time, a `PersistenceManagedTypes` bean must be declared and used by the factory bean definition, as shown by the following example: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Bean PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) { @@ -445,8 +548,28 @@ 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]] == Runtime Hints @@ -462,10 +585,17 @@ The following example makes sure that `config/app.properties` can be loaded from ====== 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. @@ -474,68 +604,130 @@ For instance, the return type of a `@Controller` method is inspected, and releva For cases that the core container cannot infer, you can register such hints programmatically. A number of convenient annotations are also provided for common use cases. - [[aot.hints.import-runtime-hints]] === `@ImportRuntimeHints` -`RuntimeHintsRegistrar` implementations allow you to get a callback to the `RuntimeHints` instance managed by the AOT engine. -Implementations of this interface can be registered using `@ImportRuntimeHints` on any Spring bean or `@Bean` factory method. -`RuntimeHintsRegistrar` implementations are detected and invoked at build time. +{spring-framework-api}/aot/hint/RuntimeHintsRegistrar.html[`RuntimeHintsRegistrar`] +implementations allow you to get a callback to the `RuntimeHints` instance managed by the +AOT engine. Implementations of this interface can be registered using +{spring-framework-api}/context/annotation/ImportRuntimeHints.html[`@ImportRuntimeHints`] +on any Spring bean or `@Bean` factory method. `RuntimeHintsRegistrar` implementations are +detected and invoked at build time. 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. +This way, if the component is not contributed to the `BeanFactory`, the hints will not 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. - [[aot.hints.reflective]] === `@Reflective` {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 +{spring-framework-api}/context/annotation/ReflectiveScan.html[`@ReflectiveScan`]. In the +example below, all types in the `com.example.app` package and its 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]] +=== `@RegisterReflection` + +{spring-framework-api}/aot/hint/annotation/RegisterReflection.html[`@RegisterReflection`] is a specialization of `@Reflective` that provides a declarative way to register reflection for arbitrary types. +NOTE: As a specialization of `@Reflective`, `@RegisterReflection` is also detected if you are using `@ReflectiveScan`. -[[aot.hints.register-reflection-for-binding]] -=== `@RegisterReflectionForBinding` +In the following example, public constructors and public methods can be invoked via reflection on `AccountService`: -{spring-framework-api}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is a specialization of `@Reflective` that registers the need for serializing arbitrary types. +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 support more specific needs. +{spring-framework-api}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is a composed annotation that is meta-annotated with `@RegisterReflection` 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. -`@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. +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.convention-based-conversion]] +=== Runtime Hints for Convention-based Conversion + +Although the core container provides built-in support for automatic conversion of many +common types (see xref:core/validation/convert.adoc[Spring Type Conversion]), some +conversions are supported via a convention-based algorithm that relies on reflection. + +Specifically, if there is no explicit `Converter` registered with the `ConversionService` +for a particular source → target type pair, the internal `ObjectToObjectConverter` +will attempt to use conventions to convert a source object to a target type by delegating +to a method on the source object or to a static factory method or constructor on the +target type. Since this convention-based algorithm can be applied to arbitrary types at +runtime, the core container is not able to infer the runtime hints necessary to support +such reflection. + +If you encounter convention-based conversion issues within a native image resulting from +lacking runtime hints, you can register the necessary hints programmatically. For +example, if your application requires a conversion from `java.time.Instant` to +`java.sql.Timestamp` and relies on `ObjectToObjectConverter` to invoke +`java.sql.Timestamp.from(Instant)` using reflection, you could implement a custom +`RuntimeHintsRegitrar` to support this use case within a native image, as demonstrated in +the following example. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class OrderService { +public class TimestampConversionRuntimeHints implements RuntimeHintsRegistrar { - @RegisterReflectionForBinding(Account.class) - public void process(Order order) { - // ... - } + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + ReflectionHints reflectionHints = hints.reflection(); + reflectionHints.registerTypeIfPresent(classLoader, "java.sql.Timestamp", hint -> hint + .withMethod("from", List.of(TypeReference.of(Instant.class)), ExecutableMode.INVOKE) + .onReachableType(TypeReference.of("java.sql.Timestamp"))); } +} ---- ====== +`TimestampConversionRuntimeHints` can then be registered declaratively via +<> or statically via a `META-INF/spring/aot.factories` +configuration file. + +[NOTE] +==== +The above `TimestampConversionRuntimeHints` class is a simplified version of the +`ObjectToObjectConverterRuntimeHints` class that is included in the framework and +registered by default. + +Thus, this specific `Instant`-to-`Timestamp` use case is already handled by the framework. +==== + [[aot.hints.testing]] === Testing Runtime Hints Spring Core also ships `RuntimeHintsPredicates`, a utility for checking that existing hints match a particular use case. -This can be used in your own tests to validate that a `RuntimeHintsRegistrar` contains the expected results. +This can be used in your own tests to validate that a `RuntimeHintsRegistrar` produces the expected results. We can write a test for our `SpellCheckService` and ensure that we will be able to load a dictionary at runtime: include-code::./SpellCheckServiceTests[tag=hintspredicates] diff --git a/framework-docs/modules/ROOT/pages/core/appendix.adoc b/framework-docs/modules/ROOT/pages/core/appendix.adoc index 5cae57dc7d05..404036a7acce 100644 --- a/framework-docs/modules/ROOT/pages/core/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/core/appendix.adoc @@ -1,7 +1,3 @@ [[appendix]] = Appendix :page-section-summary-toc: 1 - - - - diff --git a/framework-docs/modules/ROOT/pages/core/appendix/application-startup-steps.adoc b/framework-docs/modules/ROOT/pages/core/appendix/application-startup-steps.adoc index 7e33dda19a7a..d9ee87c716cf 100644 --- a/framework-docs/modules/ROOT/pages/core/appendix/application-startup-steps.adoc +++ b/framework-docs/modules/ROOT/pages/core/appendix/application-startup-steps.adoc @@ -19,10 +19,6 @@ its behavior changes. | Initialization of `SmartInitializingSingleton` beans. | `beanName` the name of the bean. -| `spring.context.annotated-bean-reader.create` -| Creation of the `AnnotatedBeanDefinitionReader`. -| - | `spring.context.base-packages.scan` | Scanning of base packages. | `packages` array of base packages for scanning. 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 5ca36a86567b..ca2fd246eda9 100644 --- a/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc +++ b/framework-docs/modules/ROOT/pages/core/appendix/xml-custom.adoc @@ -12,7 +12,6 @@ Spring's extensible XML configuration mechanism is based on XML Schema. If you a familiar with Spring's current XML configuration extensions that come with the standard Spring distribution, you should first read the previous section on xref:core/appendix/xsd-schemas.adoc[XML Schemas]. - To create new XML configuration extensions: . xref:core/appendix/xml-custom.adoc#core.appendix.xsd-custom-schema[Author] an XML schema to describe your custom element(s). @@ -38,7 +37,6 @@ examples follow later in this appendix. The intent of this first simple example through the basic steps of making a custom extension.) - [[xsd-custom-schema]] == Authoring the Schema @@ -110,7 +108,6 @@ can use autocompletion to let a user choose between several configuration option defined in the enumeration. - [[xsd-custom-namespacehandler]] == Coding a `NamespaceHandler` @@ -145,7 +142,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 +158,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 @@ -187,7 +184,6 @@ means that each `BeanDefinitionParser` contains only the logic for parsing a sin custom element, as we can see in the next step. - [[xsd-custom-parser]] == Using `BeanDefinitionParser` @@ -202,7 +198,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 +236,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 @@ -276,13 +272,11 @@ the basic grunt work of creating a single `BeanDefinition`. single `BeanDefinition` represents. ====== - In this simple case, this is all that we need to do. The creation of our single `BeanDefinition` is handled by the `AbstractSingleBeanDefinitionParser` superclass, as is the extraction and setting of the bean definition's unique identifier. - [[xsd-custom-registration]] == Registering the Handler and the Schema @@ -294,7 +288,6 @@ can, for example, be distributed alongside your binary classes in a JAR file. Th XML parsing infrastructure automatically picks up your new extension by consuming these special properties files, the formats of which are detailed in the next two sections. - [[xsd-custom-registration-spring-handlers]] === Writing `META-INF/spring.handlers` @@ -313,7 +306,6 @@ The first part (the key) of the key-value pair is the URI associated with your c namespace extension and needs to exactly match exactly the value of the `targetNamespace` attribute, as specified in your custom XSD schema. - [[xsd-custom-registration-spring-schemas]] === Writing 'META-INF/spring.schemas' @@ -337,7 +329,6 @@ You are encouraged to deploy your XSD file (or files) right alongside the `NamespaceHandler` and `BeanDefinitionParser` classes on the classpath. - [[xsd-custom-using]] == Using a Custom Extension in Your Spring XML Configuration @@ -371,13 +362,11 @@ in a Spring XML configuration file: <1> Our custom bean. - [[xsd-custom-meat]] == More Detailed Examples This section presents some more detailed examples of custom XML extensions. - [[xsd-custom-custom-nested]] === Nesting Custom Elements within Custom Elements @@ -416,7 +405,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 +438,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 +469,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 +511,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 +587,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 +603,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 +626,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 +677,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 @@ -753,7 +742,6 @@ http\://www.foo.example/schema/component=com.foo.ComponentNamespaceHandler http\://www.foo.example/schema/component/component.xsd=com/foo/component.xsd ---- - [[xsd-custom-custom-just-attributes]] === Custom Attributes on "`Normal`" Elements @@ -787,7 +775,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 +795,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 +831,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 +849,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 +874,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 +930,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 @@ -1007,5 +995,3 @@ http\://www.foo.example/schema/jcache=com.foo.JCacheNamespaceHandler # in 'META-INF/spring.schemas' http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd ---- - - 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 0752b210d7ac..c6532deebb75 100644 --- a/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc +++ b/framework-docs/modules/ROOT/pages/core/appendix/xsd-schemas.adoc @@ -4,7 +4,6 @@ This part of the appendix lists XML schemas related to the core container. - [[xsd-schemas-util]] == The `util` Schema @@ -29,7 +28,6 @@ correct schema so that the tags in the `util` namespace are available to you): ---- - [[xsd-schemas-util-constant]] === Using `` @@ -121,7 +119,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 +132,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 +150,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 +166,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 @@ -186,7 +184,6 @@ Kotlin:: ---- - [[xsd-schemas-util-property-path]] === Using `` @@ -308,7 +305,6 @@ You can specifically set the result type in the actual definition. This is not n for most use cases, but it can sometimes be useful. See the javadoc for more info on this feature. - [[xsd-schemas-util-properties]] === Using `` @@ -334,7 +330,6 @@ The following example uses a `util:properties` element to make a more concise re ---- - [[xsd-schemas-util-list]] === Using `` @@ -389,7 +384,6 @@ following configuration: If no `list-class` attribute is supplied, the container chooses a `List` implementation. - [[xsd-schemas-util-map]] === Using `` @@ -444,7 +438,6 @@ following configuration: If no `'map-class'` attribute is supplied, the container chooses a `Map` implementation. - [[xsd-schemas-util-set]] === Using `` @@ -500,7 +493,6 @@ following configuration: If no `set-class` attribute is supplied, the container chooses a `Set` implementation. - [[xsd-schemas-aop]] == The `aop` Schema @@ -530,7 +522,6 @@ are available to you): ---- - [[xsd-schemas-context]] == The `context` Schema @@ -555,7 +546,6 @@ available to you: ---- - [[xsd-schemas-context-pphc]] === Using `` @@ -599,34 +589,25 @@ element for that purpose. Similarly, Spring's xref:integration/cache/annotations.adoc[caching annotations] need to be explicitly xref:integration/cache/annotations.adoc#cache-annotation-enable[enabled] as well. - [[xsd-schemas-context-component-scan]] === Using `` -This element is detailed in the section on xref:core/beans/annotation-config.adoc[annotation-based container configuration] -. - +This element is detailed in the section on xref:core/beans/annotation-config.adoc[annotation-based container configuration]. [[xsd-schemas-context-ltw]] === Using `` -This element is detailed in the section on xref:core/aop/using-aspectj.adoc#aop-aj-ltw[load-time weaving with AspectJ in the Spring Framework] -. - +This element is detailed in the section on xref:core/aop/using-aspectj.adoc#aop-aj-ltw[load-time weaving with AspectJ in the Spring Framework]. [[xsd-schemas-context-sc]] === Using `` -This element is detailed in the section on xref:core/aop/using-aspectj.adoc#aop-atconfigurable[using AspectJ to dependency inject domain objects with Spring] -. - +This element is detailed in the section on xref:core/aop/using-aspectj.adoc#aop-atconfigurable[using AspectJ to dependency inject domain objects with Spring]. [[xsd-schemas-context-mbe]] === Using `` -This element is detailed in the section on xref:integration/jmx/naming.adoc#jmx-context-mbeanexport[configuring annotation-based MBean export] -. - +This element is detailed in the section on xref:integration/jmx/naming.adoc#jmx-context-mbeanexport[configuring annotation-based MBean export]. [[xsd-schemas-beans]] @@ -666,7 +647,3 @@ as it stands). In the case of the preceding example, you could assume that there is some logic that consumes the bean definition and sets up some caching infrastructure that uses the supplied metadata. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/beans.adoc b/framework-docs/modules/ROOT/pages/core/beans.adoc index 7def6a8c23a0..edd63ce205c4 100644 --- a/framework-docs/modules/ROOT/pages/core/beans.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans.adoc @@ -3,7 +3,3 @@ :page-section-summary-toc: 1 This chapter covers Spring's Inversion of Control (IoC) container. - - - - 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 f364ae8ed1af..2ae12e0360c0 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config.adoc @@ -20,7 +20,7 @@ can be found in the xref:core/beans/standard-annotations.adoc[relevant section]. [NOTE] ==== Annotation injection is performed before external property injection. Thus, external -configuration (e.g. XML-specified bean properties) effectively overrides the annotations +configuration (for example, XML-specified bean properties) effectively overrides the annotations for properties when wired through mixed approaches. ==== @@ -62,6 +62,3 @@ application context in which it is defined. This means that, if you put it only checks for `@Autowired` beans in your controllers, and not your services. See xref:web/webmvc/mvc-servlet.adoc[The DispatcherServlet] for more information. ==== - - - 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 21a95fccc361..476c98ba71bc 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 @@ -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 { @@ -58,7 +58,7 @@ bean is left, it is effectively primary as well: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class MovieConfiguration { @@ -76,7 +76,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class MovieConfiguration { @@ -100,7 +100,7 @@ With both variants of the preceding configuration, the following ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -113,7 +113,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -152,6 +152,3 @@ The corresponding bean definitions follow: ---- - - - 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 77e78db6149c..08dd2c5fbe98 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 @@ -14,7 +14,7 @@ this can be a plain descriptive value, 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 { @@ -28,7 +28,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -50,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 { @@ -71,7 +71,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -151,53 +151,38 @@ injected into a `Set` annotated with `@Qualifier("action")`. [TIP] ==== Letting qualifier values select against target bean names, within the type-matching -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 +candidates, does not require a `@Qualifier` annotation at the injection point. If there +is no other resolution indicator (such as a qualifier, a primary marker, or a fallback +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. +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. +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, primary, or fallback 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` @@ -213,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) @@ -226,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) @@ -244,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 { @@ -265,7 +250,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -289,7 +274,7 @@ Kotlin:: Next, you can provide the information for the candidate bean definitions. You can add `` tags as sub-elements of the `` tag and then specify the `type` and `value` to match your custom qualifier annotations. The type is matched against the -fully-qualified class name of the annotation. Alternately, as a convenience if no risk of +fully-qualified class name of the annotation. Alternatively, as a convenience if no risk of conflicting names exists, you can use the short class name. The following example demonstrates both approaches: @@ -337,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) @@ -348,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) @@ -366,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 { @@ -381,7 +366,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -421,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) @@ -436,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) @@ -453,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 @@ -462,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 @@ -479,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 { @@ -505,7 +490,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -583,6 +568,3 @@ the following example: ---- -- - - - 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 fcf746542616..b24beb12e5ab 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,31 +30,31 @@ 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) ---- ====== -[NOTE] +[TIP] ==== -As of Spring Framework 4.3, an `@Autowired` annotation on such a constructor is no longer -necessary if the target bean defines only one constructor to begin with. However, if -several constructors are available and there is no primary/default constructor, at least -one of the constructors must be annotated with `@Autowired` in order to instruct the -container which one to use. See the discussion on -xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution] for details. +An `@Autowired` annotation on such a constructor is not necessary if the target bean +defines only one constructor. However, if several constructors are available and there is +no primary or default constructor, at least one of the constructors must be annotated +with `@Autowired` in order to instruct the container which one to use. See the discussion +on xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution] +for details. ==== -You can also apply the `@Autowired` annotation to _traditional_ setter methods, -as the following example shows: +You can apply the `@Autowired` annotation to _traditional_ setter methods, as the +following example shows: [tabs] ====== 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 { @@ -84,14 +84,14 @@ Kotlin:: ---- ====== -You can also apply the annotation to methods with arbitrary names and multiple -arguments, as the following example shows: +You can apply `@Autowired` to methods with arbitrary names and multiple arguments, as the +following example shows: [tabs] ====== 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) { @@ -176,24 +176,48 @@ Kotlin:: ==== Make sure that your target components (for example, `MovieCatalog` or `CustomerPreferenceDao`) are consistently declared by the type that you use for your `@Autowired`-annotated -injection points. Otherwise, injection may fail due to a "no type match found" error at runtime. +injection points. Otherwise, injection may fail due to a "no type match found" error at +runtime. For XML-defined beans or component classes found via classpath scanning, the container usually knows the concrete type up front. However, for `@Bean` factory methods, you need to make sure that the declared return type is sufficiently expressive. For components that implement several interfaces or for components potentially referred to by their -implementation type, consider declaring the most specific return type on your factory -method (at least as specific as required by the injection points referring to your bean). +implementation type, declare the most specific return type on your factory 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] ==== -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. -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. +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 @@ -203,7 +227,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 { @@ -216,7 +240,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -234,7 +258,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 { @@ -251,7 +275,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -285,18 +309,19 @@ set of multiple matches for the specific bean type (as returned by the factory m 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. +through `@Order` values in combination with `@Primary` or `@Fallback` on a single bean +for each type. ==== Even typed `Map` instances can be autowired as long as the expected key type is `String`. -The map values contain all beans of the expected type, and the keys contain the -corresponding bean names, as the following example shows: +The map values are all beans of the expected type, and the keys are the corresponding +bean names, as the following example shows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -313,7 +338,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -338,7 +363,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 { @@ -355,7 +380,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -408,7 +433,7 @@ annotated constructor does not have to be public. ==== Alternatively, you can express the non-required nature of a particular dependency -through Java 8's `java.util.Optional`, as the following example shows: +through Java's `java.util.Optional`, as the following example shows: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -421,15 +446,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, `org.jspecify.annotations.Nullable` from JSpecify) or just leverage Kotlin's +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 { @@ -442,7 +467,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -465,7 +490,7 @@ an `ApplicationContext` object: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class MovieRecommender { @@ -481,7 +506,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MovieRecommender { @@ -498,8 +523,6 @@ class MovieRecommender { The `@Autowired`, `@Inject`, `@Value`, and `@Resource` annotations are handled by Spring `BeanPostProcessor` implementations. This means that you cannot apply these annotations within your own `BeanPostProcessor` or `BeanFactoryPostProcessor` types (if any). + These types must be 'wired up' explicitly by using XML or a Spring `@Bean` method. ==== - - - 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 0ca89cd0ab46..1a2e5410bb48 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 @@ -27,7 +27,5 @@ with the `CustomAutowireConfigurer` When multiple beans qualify as autowire candidates, the determination of a "`primary`" is as follows: If exactly one bean definition among the candidates has a `primary` -attribute set to `true`, it is selected. - - - +attribute set to `true`, it is selected. For annotation-based configuration, see +xref:core/beans/annotation-config/autowired-primary.adoc[Fine-tuning with `@Primary` or `@Fallback`]. 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..2fdf6c6d9ee3 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 @@ -96,6 +96,3 @@ Kotlin:: private lateinit var s: 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..75cad92f2082 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 { @@ -64,7 +64,3 @@ JDK 11. As of Jakarta EE 9, the package lives in `jakarta.annotation` now. If ne the `jakarta.annotation-api` artifact needs to be obtained via Maven Central now, simply to be added to the application's classpath like any other library. ==== - - - - 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 370471e57d0e..12d15bedef1f 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 { @@ -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 { @@ -142,4 +142,3 @@ Kotlin:: `ApplicationContext`. ====== -- - 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 4f5fd95cadb6..f5c85c8d8eae 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,22 +7,22 @@ ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class MovieRecommender { + @Component + public class MovieRecommender { - private final String catalog; + private final String catalog; - public MovieRecommender(@Value("${catalog.name}") String catalog) { - this.catalog = catalog; - } - } + public MovieRecommender(@Value("${catalog.name}") String catalog) { + this.catalog = catalog; + } + } ---- Kotlin:: + -[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,16 +35,16 @@ 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") - public class AppConfig { } + @Configuration + @PropertySource("classpath:application.properties") + public class AppConfig { } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @PropertySource("classpath:application.properties") @@ -56,7 +56,7 @@ And the following `application.properties` file: [source,java,indent=0,subs="verbatim,quotes"] ---- - catalog.name=MovieCatalog + catalog.name=MovieCatalog ---- In that case, the `catalog` parameter and field will be equal to the `MovieCatalog` value. @@ -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,11 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig Using the above configuration ensures Spring initialization failure if any `${}` placeholder could not be resolved. It is also possible to use methods like -`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or -`setEscapeCharacter` to customize placeholders. +`setPlaceholderPrefix()`, `setPlaceholderSuffix()`, `setValueSeparator()`, or +`setEscapeCharacter()` to customize the placeholder syntax. In addition, the default +escape character can be changed or disabled globally by setting the +`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via +the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism). NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that will get properties from `application.properties` and `application.yml` files. @@ -117,22 +120,22 @@ 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 { + @Component + public class MovieRecommender { - private final String catalog; + private final String catalog; - public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) { - this.catalog = catalog; - } - } + public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) { + this.catalog = catalog; + } + } ---- Kotlin:: + -[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,23 +151,23 @@ 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 { + @Configuration + public class AppConfig { - @Bean - public ConversionService conversionService() { - DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); - conversionService.addConverter(new MyCustomConverter()); - return conversionService; - } - } + @Bean + public ConversionService conversionService() { + DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); + conversionService.addConverter(new MyCustomConverter()); + return conversionService; + } + } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -186,22 +189,22 @@ 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 { + @Component + public class MovieRecommender { - private final String catalog; + private final String catalog; - public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) { - this.catalog = catalog; - } - } + public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) { + this.catalog = catalog; + } + } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class MovieRecommender( @@ -215,28 +218,26 @@ 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 { + @Component + public class MovieRecommender { - private final Map countOfMoviesPerCatalog; + private final Map countOfMoviesPerCatalog; - public MovieRecommender( - @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map countOfMoviesPerCatalog) { - this.countOfMoviesPerCatalog = countOfMoviesPerCatalog; - } - } + public MovieRecommender( + @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map countOfMoviesPerCatalog) { + this.countOfMoviesPerCatalog = countOfMoviesPerCatalog; + } + } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component class MovieRecommender( @Value("#{{'Thriller': 100, 'Comedy': 300}}") private val countOfMoviesPerCatalog: Map) ---- ====== - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc index 7102d7ada6be..ea07e2be83df 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc @@ -29,7 +29,6 @@ created and initialized, you have a fully configured and executable system or ap image::container-magic.png[] - [[beans-factory-metadata]] == Configuration Metadata @@ -63,8 +62,6 @@ 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. - - [[beans-factory-xml]] === XML as an External Configuration DSL @@ -110,16 +107,16 @@ as the local file system, the Java `CLASSPATH`, and so on. ====== 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") + val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") ---- ====== @@ -188,7 +185,6 @@ definition. This linkage between `id` and `ref` elements expresses the dependenc collaborating objects. For details of configuring an object's dependencies, see xref:core/beans/dependencies.adoc[Dependencies]. - [[beans-factory-xml-import]] === Composing XML-based Configuration Metadata @@ -206,18 +202,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 @@ -244,42 +239,6 @@ The namespace itself provides the import directive feature. Further configuration features beyond plain bean definitions are available in a selection of XML namespaces provided by Spring -- for example, the `context` and `util` namespaces. - -[[beans-factory-groovy]] -=== The Groovy Bean Definition DSL - -As a further example for externalized configuration metadata, bean definitions can also -be expressed in Spring's Groovy Bean Definition DSL, as known from the Grails framework. -Typically, such configuration live in a ".groovy" file with the structure shown in the -following example: - -[source,groovy,indent=0,subs="verbatim,quotes"] ----- - beans { - dataSource(BasicDataSource) { - driverClassName = "org.hsqldb.jdbcDriver" - url = "jdbc:hsqldb:mem:grailsDB" - username = "sa" - password = "" - settings = [mynew:"setting"] - } - sessionFactory(SessionFactory) { - dataSource = dataSource - } - myService(MyService) { - nestedBean = { AnotherBean bean -> - dataSource = dataSource - } - } - } ----- - -This configuration style is largely equivalent to XML bean definitions and even -supports Spring's XML configuration namespaces. It also allows for importing XML -bean definition files through an `importBeans` directive. - - - [[beans-factory-client]] == Using the Container @@ -294,7 +253,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"); @@ -308,18 +267,18 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - import org.springframework.beans.factory.getBean + import org.springframework.beans.factory.getBean // create and configure beans - val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") + val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") - // retrieve configured instance - val service = context.getBean("petStore") + // retrieve configured instance + val service = context.getBean("petStore") - // use configured instance - var userList = service.getUsernameList() + // use configured instance + var userList = service.getUsernameList() ---- ====== @@ -331,14 +290,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") ---- @@ -352,7 +311,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"); @@ -361,7 +320,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") @@ -376,7 +335,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"); @@ -385,7 +344,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") @@ -403,6 +362,3 @@ code should never use them. Indeed, your application code should have no calls t Spring's integration with web frameworks provides dependency injection for various web framework components such as controllers and JSF-managed beans, letting you declare a dependency on a specific bean through metadata (such as an autowiring annotation). - - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/beanfactory.adoc b/framework-docs/modules/ROOT/pages/core/beans/beanfactory.adoc index 32837957748c..20d280d8ba1b 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/beanfactory.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/beanfactory.adoc @@ -21,7 +21,6 @@ operate on shared `BeanDefinition` objects as a core metadata representation. This is the essence of what makes Spring's container so flexible and extensible. - [[context-introduction-ctx-vs-beanfactory]] == `BeanFactory` or `ApplicationContext`? @@ -90,7 +89,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 +103,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 +123,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 +139,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/child-bean-definitions.adoc b/framework-docs/modules/ROOT/pages/core/beans/child-bean-definitions.adoc index 2c4d287a08f4..36d389d9090b 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/child-bean-definitions.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/child-bean-definitions.adoc @@ -78,7 +78,3 @@ important (at least for singleton beans) that if you have a (parent) bean defini which you intend to use only as a template, and this definition specifies a class, you must make sure to set the __abstract__ attribute to __true__, otherwise the application context will actually (attempt to) pre-instantiate the `abstract` bean. - - - - 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 85d354a82c6e..4e80edb40f32 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc @@ -1,28 +1,29 @@ [[beans-classpath-scanning]] = Classpath Scanning and Managed Components -Most examples in this chapter use XML to specify the configuration metadata that produces -each `BeanDefinition` within the Spring container. The previous section -(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration]) demonstrates how to provide a lot of the configuration -metadata through source-level annotations. Even in those examples, however, the "base" -bean definitions are explicitly defined in the XML file, while the annotations drive only -the dependency injection. This section describes an option for implicitly detecting the -candidate components by scanning the classpath. Candidate components are classes that -match against a filter criteria and have a corresponding bean definition registered with -the container. This removes the need to use XML to perform bean registration. Instead, you -can use annotations (for example, `@Component`), AspectJ type expressions, or your own +Most examples in this chapter use XML to specify the configuration metadata that +produces each `BeanDefinition` within the Spring container. The previous section +(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration]) +demonstrates how to provide a lot of the configuration metadata through source-level +annotations. Even in those examples, however, the "base" bean definitions are explicitly +defined in the XML file, while the annotations drive only the dependency injection. + +This section describes an option for implicitly detecting the candidate components by +scanning the classpath. Candidate components are classes that match against filter +criteria and have a corresponding bean definition registered with the container. +This removes the need to use XML to perform bean registration. Instead, you can use +annotations (for example, `@Component`), AspectJ type expressions, or your own custom filter criteria to select which classes have bean definitions registered with the container. [NOTE] ==== You can define beans using Java rather than using XML files. Take a look at the -`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples of how to -use these features. +`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples +of how to use these features. ==== - [[beans-stereotype-annotations]] == `@Component` and Further Stereotype Annotations @@ -46,7 +47,6 @@ clearly the better choice. Similarly, as stated earlier, `@Repository` is alread supported as a marker for automatic exception translation in your persistence layer. - [[beans-meta-annotations]] == Using Meta-annotations and Composed Annotations @@ -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) @@ -70,11 +70,11 @@ Java:: // ... } ---- -<1> The `@Component` causes `@Service` to be treated in the same way as `@Component`. +<1> The `@Component` meta-annotation causes `@Service` to be treated in the same way as `@Component`. Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @@ -85,7 +85,7 @@ Kotlin:: // ... } ---- -<1> The `@Component` causes `@Service` to be treated in the same way as `@Component`. +<1> The `@Component` meta-annotation causes `@Service` to be treated in the same way as `@Component`. ====== You can also combine meta-annotations to create "`composed annotations`". For example, @@ -97,13 +97,13 @@ meta-annotations to allow customization. This can be particularly useful when yo want to only expose a subset of the meta-annotation's attributes. For example, Spring's `@SessionScope` annotation hard codes the scope name to `session` but still allows customization of the `proxyMode`. The following listing shows the definition of the -`SessionScope` annotation: +`@SessionScope` annotation: [tabs] ====== 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) @@ -195,7 +195,6 @@ For further details, see the wiki page. - [[beans-scanning-autodetection]] == Automatically Detecting Classes and Registering Bean Definitions @@ -207,12 +206,12 @@ 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 { - private MovieFinder movieFinder; + private final MovieFinder movieFinder; public SimpleMovieLister(MovieFinder movieFinder) { this.movieFinder = movieFinder; @@ -222,7 +221,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 +232,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 +242,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repository class JpaMovieFinder : MovieFinder { @@ -252,17 +251,17 @@ Kotlin:: ---- ====== - To autodetect these classes and register the corresponding beans, you need to add -`@ComponentScan` to your `@Configuration` class, where the `basePackages` attribute -is a common parent package for the two classes. (Alternatively, you can specify a -comma- or semicolon- or space-separated list that includes the parent package of each class.) +`@ComponentScan` to your `@Configuration` class, where the `basePackages` attribute is +configured with a common parent package for the two classes. Alternatively, you can +specify a comma-, semicolon-, or space-separated list that includes the parent package +of each class. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = "org.example") @@ -273,7 +272,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = ["org.example"]) @@ -283,10 +282,10 @@ Kotlin:: ---- ====== -NOTE: For brevity, the preceding example could have used the `value` attribute of the -annotation (that is, `@ComponentScan("org.example")`). +TIP: For brevity, the preceding example could have used the implicit `value` attribute of +the annotation instead: `@ComponentScan("org.example")` -The following alternative uses XML: +The following example uses XML configuration: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -317,8 +316,8 @@ exposed based on security policies in some environments -- for example, standalo JDK 1.7.0_45 and higher (which requires 'Trusted-Library' setup in your manifests -- see {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). @@ -326,17 +325,68 @@ sure that they are 'opened' (that is, that they use an `opens` declaration inste Furthermore, the `AutowiredAnnotationBeanPostProcessor` and `CommonAnnotationBeanPostProcessor` are both implicitly included when you use the -component-scan element. That means that the two components are autodetected and -wired together -- all without any bean configuration metadata provided in XML. +`` element. That means that the two components are autodetected +and wired together -- all without any bean configuration metadata provided in XML. NOTE: You can disable the registration of `AutowiredAnnotationBeanPostProcessor` and `CommonAnnotationBeanPostProcessor` by including the `annotation-config` attribute with a value of `false`. +[[beans-scanning-placeholders-and-patterns]] +=== Property Placeholders and Ant-style Patterns + +The `basePackages` and `value` attributes in `@ComponentScan` support `${...}` property +placeholders which are resolved against the `Environment` as well as Ant-style package +patterns such as `"org.example.+++**+++"`. + +In addition, multiple packages or patterns may be specified, either separately or within +a single String — for example, `{"org.example.config", "org.example.service.+++**+++"}` +or `"org.example.config, org.example.service.+++**+++"`. + +The following example specifies the `app.scan.packages` property placeholder for the +implicit `value` attribute in `@ComponentScan`. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Configuration +@ComponentScan("${app.scan.packages}") // <1> +public class AppConfig { + // ... +} +---- +<1> `app.scan.packages` property placeholder to be resolved against the `Environment` + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- +@Configuration +@ComponentScan(["\${app.scan.packages}"]) // <1> +class AppConfig { + // ... +} +---- +<1> `app.scan.packages` property placeholder to be resolved against the `Environment` +====== + +The following listing represents a properties file which defines the `app.scan.packages` +property. In the preceding example, it is assumed that this properties file has been +registered with the `Environment` – for example, via `@PropertySource` or a similar +mechanism. + +[source,properties,indent=0,subs="verbatim,quotes"] +---- +app.scan.packages=org.example.config, org.example.service.** +---- + [[beans-scanning-filters]] -== Using Filters to Customize Scanning +=== Using Filters to Customize Scanning By default, classes annotated with `@Component`, `@Repository`, `@Service`, `@Controller`, `@Configuration`, or a custom annotation that itself is annotated with `@Component` are @@ -373,14 +423,14 @@ The following table describes the filtering options: | A custom implementation of the `org.springframework.core.type.TypeFilter` interface. |=== -The following example shows the configuration ignoring all `@Repository` annotations -and using "`stub`" repositories instead: +The following example shows `@ComponentScan` configuration that excludes all +`@Repository` annotations and includes "`Stub`" repositories instead: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = "org.example", @@ -393,7 +443,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan(basePackages = ["org.example"], @@ -426,242 +476,8 @@ annotated or meta-annotated with `@Component`, `@Repository`, `@Service`, `@Cont `@RestController`, or `@Configuration`. - -[[beans-factorybeans-annotations]] -== Defining Bean Metadata within Components - -Spring components can also contribute bean definition metadata to the container. You can do -this with the same `@Bean` annotation used to define bean metadata within `@Configuration` -annotated classes. The following example shows how to do so: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Component - public class FactoryMethodComponent { - - @Bean - @Qualifier("public") - public TestBean publicInstance() { - return new TestBean("publicInstance"); - } - - public void doWork() { - // Component method implementation omitted - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Component - class FactoryMethodComponent { - - @Bean - @Qualifier("public") - fun publicInstance() = TestBean("publicInstance") - - fun doWork() { - // Component method implementation omitted - } - } ----- -====== - -The preceding class is a Spring component that has application-specific code in its -`doWork()` method. However, it also contributes a bean definition that has a factory -method referring to the method `publicInstance()`. The `@Bean` annotation identifies the -factory method and other bean definition properties, such as a qualifier value through -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` -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: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Component - public class FactoryMethodComponent { - - private static int i; - - @Bean - @Qualifier("public") - public TestBean publicInstance() { - return new TestBean("publicInstance"); - } - - // use of a custom qualifier and autowiring of method parameters - @Bean - protected TestBean protectedInstance( - @Qualifier("public") TestBean spouse, - @Value("#{privateInstance.age}") String country) { - TestBean tb = new TestBean("protectedInstance", 1); - tb.setSpouse(spouse); - tb.setCountry(country); - return tb; - } - - @Bean - private TestBean privateInstance() { - return new TestBean("privateInstance", i++); - } - - @Bean - @RequestScope - public TestBean requestScopedInstance() { - return new TestBean("requestScopedInstance", 3); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Component - class FactoryMethodComponent { - - companion object { - private var i: Int = 0 - } - - @Bean - @Qualifier("public") - fun publicInstance() = TestBean("publicInstance") - - // use of a custom qualifier and autowiring of method parameters - @Bean - protected fun protectedInstance( - @Qualifier("public") spouse: TestBean, - @Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply { - this.spouse = spouse - this.country = country - } - - @Bean - private fun privateInstance() = TestBean("privateInstance", i++) - - @Bean - @RequestScope - fun requestScopedInstance() = TestBean("requestScopedInstance", 3) - } ----- -====== - -The example autowires the `String` method parameter `country` to the value of the `age` -property on another bean named `privateInstance`. A Spring Expression Language element -defines the value of the property through the notation `#{ }`. For `@Value` -annotations, an expression resolver is preconfigured to look for bean names when -resolving expression text. - -As of Spring Framework 4.3, you may also declare a factory method parameter of type -`InjectionPoint` (or its more specific subclass: `DependencyDescriptor`) to -access the requesting injection point that triggers the creation of the current bean. -Note that this applies only to the actual creation of bean instances, not to the -injection of existing instances. As a consequence, this feature makes most sense for -beans of prototype scope. For other scopes, the factory method only ever sees the -injection point that triggered the creation of a new bean instance in the given scope -(for example, the dependency that triggered the creation of a lazy singleton bean). -You can use the provided injection point metadata with semantic care in such scenarios. -The following example shows how to use `InjectionPoint`: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Component - public class FactoryMethodComponent { - - @Bean @Scope("prototype") - public TestBean prototypeInstance(InjectionPoint injectionPoint) { - return new TestBean("prototypeInstance for " + injectionPoint.getMember()); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Component - class FactoryMethodComponent { - - @Bean - @Scope("prototype") - fun prototypeInstance(injectionPoint: InjectionPoint) = - TestBean("prototypeInstance for ${injectionPoint.member}") - } ----- -====== - -The `@Bean` methods in a regular Spring component are processed differently than their -counterparts inside a Spring `@Configuration` class. The difference is that `@Component` -classes are not enhanced with CGLIB to intercept the invocation of methods and fields. -CGLIB proxying is the means by which invoking methods or fields within `@Bean` methods -in `@Configuration` classes creates bean metadata references to collaborating objects. -Such methods are not invoked with normal Java semantics but rather go through the -container in order to provide the usual lifecycle management and proxying of Spring -beans, even when referring to other beans through programmatic calls to `@Bean` methods. -In contrast, invoking a method or field in a `@Bean` method within a plain `@Component` -class has standard Java semantics, with no special CGLIB processing or other -constraints applying. - -[NOTE] -==== -You may declare `@Bean` methods as `static`, allowing for them to be called without -creating their containing configuration class as an instance. This makes particular -sense when defining post-processor beans (for example, of type `BeanFactoryPostProcessor` -or `BeanPostProcessor`), since such beans get initialized early in the container -lifecycle and should avoid triggering other parts of the configuration at that point. - -Calls to static `@Bean` methods never get intercepted by the container, not even within -`@Configuration` classes (as described earlier in this section), due to technical -limitations: CGLIB subclassing can override only non-static methods. As a consequence, -a direct call to another `@Bean` method has standard Java semantics, resulting -in an independent instance being returned straight from the factory method itself. - -The Java language visibility of `@Bean` methods does not have an immediate impact on -the resulting bean definition in Spring's container. You can freely declare your -factory methods as you see fit in non-`@Configuration` classes and also for static -methods anywhere. However, regular `@Bean` methods in `@Configuration` classes need -to be overridable -- that is, they must not be declared as `private` or `final`. - -`@Bean` methods are also discovered on base classes of a given component or -configuration class, as well as on Java 8 default methods declared in interfaces -implemented by the component or configuration class. This allows for a lot of -flexibility in composing complex configuration arrangements, with even multiple -inheritance being possible through Java 8 default methods as of Spring 4.2. - -Finally, a single class may hold multiple `@Bean` methods for the same -bean, as an arrangement of multiple factory methods to use depending on available -dependencies at runtime. This is the same algorithm as for choosing the "`greediest`" -constructor or factory method in other configuration scenarios: The variant with -the largest number of satisfiable dependencies is picked at construction time, -analogous to how the container selects between multiple `@Autowired` constructors. -==== - - - [[beans-scanning-name-generator]] -== Naming Autodetected Components +=== Naming Autodetected Components When a component is autodetected as part of the scanning process, its bean name is generated by the `BeanNameGenerator` strategy known to that scanner. @@ -670,9 +486,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 +513,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 +523,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 +536,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 +546,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repository class MovieFinderImpl : MovieFinder { @@ -742,24 +556,17 @@ 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 -{spring-framework-api}/beans/factory/support/BeanNameGenerator.html[`BeanNameGenerator`] +bean-naming strategy. First, implement either the +{spring-framework-api}/beans/factory/support/BeanNameGenerator.html[`BeanNameGenerator`] or +{spring-framework-api}/context/annotation/ConfigurationBeanNameGenerator.html[`ConfigurationBeanNameGenerator`] 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. - -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 -`FullyQualifiedAnnotationBeanNameGenerator` located in package -`org.springframework.context.annotation` can be used for such purposes. +qualified class name when configuring the scanner, as the following examples show. [tabs] ====== 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 +577,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) @@ -788,14 +595,34 @@ Kotlin:: ---- +[TIP] +==== +If you run into naming conflicts due to multiple autodetected components having the same +non-qualified class name (for example, classes with identical names but residing in +different packages), you can configure a `BeanNameGenerator` that defaults to the +fully-qualified class name for the generated bean name. The +`FullyQualifiedAnnotationBeanNameGenerator` can be used for such purposes. + +As of Spring Framework 7.0, if you encounter naming conflicts among `@Bean` methods in +`@Configuration` classes, you can alternatively configure a +`ConfigurationBeanNameGenerator` that generates unique bean names for `@Bean` methods. +The `FullyQualifiedConfigurationBeanNameGenerator` can be used to generate +fully-qualified default bean names for `@Bean` methods without an explicit `name` +attribute — for example, `com.example.MyConfig.myBean` for an `@Bean` method named +`myBean()` declared in `@Configuration` class `com.example.MyConfig`. + +The `FullyQualifiedAnnotationBeanNameGenerator` and +`FullyQualifiedConfigurationBeanNameGenerator` both reside in the +`org.springframework.context.annotation` package. +==== + As a general rule, consider specifying the name with the annotation whenever other components may be making explicit references to it. On the other hand, the auto-generated names are adequate whenever the container is responsible for wiring. - [[beans-scanning-scope-resolver]] -== Providing a Scope for Autodetected Components +=== Providing a Scope for Autodetected Components As with Spring-managed components in general, the default and most common scope for autodetected components is `singleton`. However, sometimes you need a different scope @@ -806,7 +633,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 +644,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Scope("prototype") @Repository @@ -833,10 +660,10 @@ definitions, there is no notion of bean definition inheritance, and inheritance hierarchies at the class level are irrelevant for metadata purposes. For details on web-specific scopes such as "`request`" or "`session`" in a Spring context, -see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes]. As with the pre-built annotations for those scopes, -you may also compose your own scoping annotations by using Spring's meta-annotation -approach: for example, a custom annotation meta-annotated with `@Scope("prototype")`, -possibly also declaring a custom scoped-proxy mode. +see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes]. +As with the pre-built annotations for those scopes, you may also compose your own scoping +annotations by using Spring's meta-annotation approach: for example, a custom annotation +meta-annotated with `@Scope("prototype")`, 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 @@ -849,7 +676,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 +687,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) @@ -878,7 +705,8 @@ Kotlin:: ---- When using certain non-singleton scopes, it may be necessary to generate proxies for the -scoped objects. The reasoning is described in xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies]. +scoped objects. The reasoning is described in +xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies]. For this purpose, a scoped-proxy attribute is available on the component-scan element. The three possible values are: `no`, `interfaces`, and `targetClass`. For example, the following configuration results in standard JDK dynamic proxies: @@ -887,7 +715,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 +726,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) @@ -916,11 +744,11 @@ Kotlin:: ---- - [[beans-scanning-qualifiers]] -== Providing Qualifier Metadata with Annotations +=== 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 +762,7 @@ technique: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component @Qualifier("Action") @@ -945,7 +773,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component @Qualifier("Action") @@ -957,7 +785,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component @Genre("Action") @@ -968,7 +796,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component @Genre("Action") @@ -982,7 +810,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Component @Offline @@ -993,7 +821,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Component @Offline @@ -1009,5 +837,237 @@ of the same type to provide variations in their qualifier metadata, because that metadata is provided per-instance rather than per-class. +[[beans-factorybeans-annotations]] +== Defining Bean Metadata within Components + +Spring components can also contribute bean definition metadata to the container. You can do +this with the same `@Bean` annotation used to define bean metadata within `@Configuration` +annotated classes. The following example shows how to do so: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Component + public class FactoryMethodComponent { + + @Bean + @Qualifier("public") + public TestBean publicInstance() { + return new TestBean("publicInstance"); + } + + public void doWork() { + // Component method implementation omitted + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Component + class FactoryMethodComponent { + + @Bean + @Qualifier("public") + fun publicInstance() = TestBean("publicInstance") + + fun doWork() { + // Component method implementation omitted + } + } +---- +====== + +The preceding class is a Spring component that has application-specific code in its +`doWork()` method. However, it also contributes a bean definition that has a factory +method referring to the method `publicInstance()`. The `@Bean` annotation identifies the +factory method and other bean definition properties, such as a qualifier value through +the `@Qualifier` annotation. Other method-level annotations that can be specified are +`@Scope`, `@Lazy`, and custom qualifier annotations. + +[[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: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Component + public class FactoryMethodComponent { + + private static int i; + + @Bean + @Qualifier("public") + public TestBean publicInstance() { + return new TestBean("publicInstance"); + } + + // use of a custom qualifier and autowiring of method parameters + @Bean + protected TestBean protectedInstance( + @Qualifier("public") TestBean spouse, + @Value("#{privateInstance.age}") String country) { + TestBean tb = new TestBean("protectedInstance", 1); + tb.setSpouse(spouse); + tb.setCountry(country); + return tb; + } + + @Bean + private TestBean privateInstance() { + return new TestBean("privateInstance", i++); + } + + @Bean + @RequestScope + public TestBean requestScopedInstance() { + return new TestBean("requestScopedInstance", 3); + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Component + class FactoryMethodComponent { + + companion object { + private var i: Int = 0 + } + + @Bean + @Qualifier("public") + fun publicInstance() = TestBean("publicInstance") + + // use of a custom qualifier and autowiring of method parameters + @Bean + protected fun protectedInstance( + @Qualifier("public") spouse: TestBean, + @Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply { + this.spouse = spouse + this.country = country + } + + @Bean + private fun privateInstance() = TestBean("privateInstance", i++) + + @Bean + @RequestScope + fun requestScopedInstance() = TestBean("requestScopedInstance", 3) + } +---- +====== + +The example autowires the `String` method parameter `country` to the value of the `age` +property on another bean named `privateInstance`. A Spring Expression Language element +defines the value of the property through the notation `#{ }`. For `@Value` +annotations, an expression resolver is preconfigured to look for bean names when +resolving expression text. + +As of Spring Framework 4.3, you may also declare a factory method parameter of type +`InjectionPoint` (or its more specific subclass: `DependencyDescriptor`) to +access the requesting injection point that triggers the creation of the current bean. +Note that this applies only to the actual creation of bean instances, not to the +injection of existing instances. As a consequence, this feature makes most sense for +beans of prototype scope. For other scopes, the factory method only ever sees the +injection point that triggered the creation of a new bean instance in the given scope +(for example, the dependency that triggered the creation of a lazy singleton bean). +You can use the provided injection point metadata with semantic care in such scenarios. +The following example shows how to use `InjectionPoint`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Component + public class FactoryMethodComponent { + + @Bean @Scope("prototype") + public TestBean prototypeInstance(InjectionPoint injectionPoint) { + return new TestBean("prototypeInstance for " + injectionPoint.getMember()); + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Component + class FactoryMethodComponent { + + @Bean + @Scope("prototype") + fun prototypeInstance(injectionPoint: InjectionPoint) = + TestBean("prototypeInstance for ${injectionPoint.member}") + } +---- +====== + +The `@Bean` methods in a regular Spring component are processed differently than their +counterparts inside a Spring `@Configuration` class. The difference is that `@Component` +classes are not enhanced with CGLIB to intercept the invocation of methods and fields. +CGLIB proxying is the means by which invoking methods or fields within `@Bean` methods +in `@Configuration` classes creates bean metadata references to collaborating objects. +Such methods are not invoked with normal Java semantics but rather go through the +container in order to provide the usual lifecycle management and proxying of Spring +beans, even when referring to other beans through programmatic calls to `@Bean` methods. +In contrast, invoking a method or field in a `@Bean` method within a plain `@Component` +class has standard Java semantics, with no special CGLIB processing or other +constraints applying. + +[NOTE] +==== +You may declare `@Bean` methods as `static`, allowing for them to be called without +creating their containing configuration class as an instance. This makes particular +sense when defining post-processor beans (for example, of type `BeanFactoryPostProcessor` +or `BeanPostProcessor`), since such beans get initialized early in the container +lifecycle and should avoid triggering other parts of the configuration at that point. + +Calls to static `@Bean` methods never get intercepted by the container, not even within +`@Configuration` classes (as described earlier in this section), due to technical +limitations: CGLIB subclassing can override only non-static methods. As a consequence, +a direct call to another `@Bean` method has standard Java semantics, resulting +in an independent instance being returned straight from the factory method itself. + +The Java language visibility of `@Bean` methods does not have an immediate impact on +the resulting bean definition in Spring's container. You can freely declare your +factory methods as you see fit in non-`@Configuration` classes and also for static +methods anywhere. However, regular `@Bean` methods in `@Configuration` classes need +to be overridable -- that is, they must not be declared as `private` or `final`. +`@Bean` methods are also discovered on base classes of a given component or +configuration class, as well as on Java default methods declared in interfaces +implemented by the component or configuration class. This allows for a lot of +flexibility in composing complex configuration arrangements, with even multiple +inheritance being possible through Java default methods. +Finally, a single class may hold multiple `@Bean` methods for the same +bean, as an arrangement of multiple factory methods to use depending on available +dependencies at runtime. This is the same algorithm as for choosing the "`greediest`" +constructor or factory method in other configuration scenarios: The variant with +the largest number of satisfiable dependencies is picked at construction time, +analogous to how the container selects between multiple `@Autowired` constructors. +==== 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 ac5192b0357f..5c9a06d1620a 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc @@ -1,7 +1,7 @@ [[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 {spring-framework-api}/context/ApplicationContext.html[`ApplicationContext`] @@ -24,7 +24,6 @@ package also provides the following functionality: `HierarchicalBeanFactory` interface. - [[context-functionality-messagesource]] == Internationalization using `MessageSource` @@ -102,7 +101,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 +112,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 +160,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 +180,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Example { @@ -224,7 +223,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 +235,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") @@ -273,7 +272,6 @@ See the {spring-framework-api}/context/support/ReloadableResourceBundleMessageSo javadoc for details. - [[context-functionality-events]] == Standard and Custom Events @@ -344,7 +342,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 +361,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 +378,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 +405,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class EmailService : ApplicationEventPublisherAware { @@ -447,7 +445,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 +463,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class BlockedListNotifier : ApplicationListener { @@ -484,7 +482,7 @@ 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, +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. @@ -513,7 +511,7 @@ the classes above: - + @@ -534,7 +532,6 @@ complete support for building lightweight, https://www.enterpriseintegrationpatterns.com[pattern-oriented], event-driven architectures that build upon the well-known Spring programming model. - [[context-functionality-events-annotation]] === Annotation-based Event Listeners @@ -545,7 +542,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 +561,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class BlockedListNotifier { @@ -578,6 +575,8 @@ Kotlin:: ---- ====== +NOTE: Do not define such beans to be lazy as the `ApplicationContext` will honor 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 +590,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 +600,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 +620,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 +630,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 +643,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 +659,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 +676,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 +687,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 { @@ -705,7 +704,6 @@ The `handleBlockedListEvent()` method publishes a new `ListUpdateEvent` for ever `BlockedListEvent` that it handles. If you need to publish several events, you can return a `Collection` or an array of events instead. - [[context-functionality-events-async]] === Asynchronous Listeners @@ -717,7 +715,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 +726,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @EventListener @Async @@ -752,7 +750,6 @@ Be aware of the following limitations when using asynchronous events: See xref:integration/observability.adoc#observability.application-events[the `@EventListener` Observability section] for more information on Observability concerns. - [[context-functionality-events-order]] === Ordering Listeners @@ -763,7 +760,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 +771,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @EventListener @Order(42) @@ -784,7 +781,6 @@ Kotlin:: ---- ====== - [[context-functionality-events-generics]] === Generic Events @@ -797,7 +793,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 +803,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 +825,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 +842,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 +860,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"] ---- @@ -878,16 +874,16 @@ e.g. for processing all events asynchronously and/or for handling listener excep ---- - [[context-functionality-resources]] == 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 @@ -910,7 +906,6 @@ with special prefixes to force loading of definitions from the classpath or a UR regardless of the actual context type. - [[context-functionality-startup]] == Application Startup Tracking @@ -935,30 +930,28 @@ 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"); - // add tagging information to the current step - scanPackages.tag("packages", () -> Arrays.toString(basePackages)); - // perform the actual phase we're instrumenting - this.scanner.scan(basePackages); - // end the current step - scanPackages.end(); + try (StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan")) { + // add tagging information to the current step + scanPackages.tag("packages", () -> Arrays.toString(basePackages)); + // perform the actual phase we're instrumenting + this.scanner.scan(basePackages); + } ---- 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") - // add tagging information to the current step - scanPackages.tag("packages", () -> Arrays.toString(basePackages)) - // perform the actual phase we're instrumenting - this.scanner.scan(basePackages) - // end the current step - scanPackages.end() + try (val scanPackages = getApplicationStartup().start("spring.context.base-packages.scan")) { + // add tagging information to the current step + scanPackages.tag("packages", () -> Arrays.toString(basePackages)); + // perform the actual phase we're instrumenting + this.scanner.scan(basePackages); + } ---- ====== @@ -987,6 +980,7 @@ or ask for the `ApplicationStartup` type on any injection point. NOTE: Developers should not use the `"spring.*"` namespace when creating custom startup steps. This namespace is reserved for internal Spring usage and is subject to change. + [[context-create]] == Convenient ApplicationContext Instantiation for Web Applications @@ -1019,7 +1013,6 @@ Examples are `/WEB-INF/{asterisk}Context.xml` (for all files with names that end (for all such files in any subdirectory of `WEB-INF`). - [[context-deploy-rar]] == Deploying a Spring `ApplicationContext` as a Jakarta EE RAR File @@ -1050,7 +1043,8 @@ 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 {spring-framework-api}/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 @@ -1063,7 +1057,3 @@ other modules. A RAR-based `ApplicationContext` may also, for example, schedule or react to new files in the file system (or the like). If it needs to allow synchronous access from the outside, it could (for example) export RMI endpoints, which may be used by other application modules on the same machine. - - - - 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 25943592c078..ea25df7e9ea0 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 @@ -45,8 +45,5 @@ xref:data-access/orm/jpa.adoc[Spring's JPA support] where load-time weaving may necessary for JPA class transformation. Consult the {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]. - - - - +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 9a407ea48c8f..c010f46148c2 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. @@ -73,23 +73,35 @@ runtime (concurrently with live access to the factory) is not officially support lead to concurrent access exceptions, inconsistent state in the bean container, or both. ==== + [[beans-definition-overriding]] == Overriding Beans -Bean overriding is happening when a bean is registered using an identifier that is -already allocated. While bean overriding is possible, it makes the configuration harder -to read and this feature will be deprecated in a future release. +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` -to `false` on the `ApplicationContext` before it is refreshed. In such setup, an +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 bean overriding 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`. +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. Please refer to +xref:testing/testcontext-framework/bean-overriding.adoc[this section] for more details. -NOTE: We acknowledge that overriding beans in a test is convenient, and there is -explicit support for this. For more details please refer to xref:testing/testcontext-framework/bean-overriding.adoc[this section]. [[beans-beanname]] == Naming Beans @@ -133,7 +145,6 @@ case when there is more than one character and both the first and second charact are upper case, the original casing gets preserved. These are the same rules as defined by `java.beans.Introspector.decapitalize` (which Spring uses here). - [[beans-beanname-alias]] === Aliasing a Bean outside the Bean Definition @@ -183,7 +194,6 @@ See xref:core/beans/java/bean-annotation.adoc[Using the `@Bean` Annotation] for **** - [[beans-factory-class]] == Instantiating Beans @@ -195,7 +205,8 @@ If you use XML-based configuration metadata, you specify the type (or class) of that is to be instantiated in the `class` attribute of the `` element. This `class` attribute (which, internally, is a `Class` property on a `BeanDefinition` instance) is usually mandatory. (For exceptions, see -xref:core/beans/definition.adoc#beans-factory-class-instance-factory-method[Instantiation by Using an Instance Factory Method] and xref:core/beans/child-bean-definitions.adoc[Bean Definition Inheritance].) +xref:core/beans/definition.adoc#beans-factory-class-instance-factory-method[Instantiation by Using an Instance Factory Method] +and xref:core/beans/child-bean-definitions.adoc[Bean Definition Inheritance].) You can use the `Class` property in one of two ways: * Typically, to specify the bean class to be constructed in the case where the container @@ -219,7 +230,6 @@ a bean definition would be `com.example.SomeThing$OtherThing` or `com.example.SomeThing.OtherThing`. **** - [[beans-factory-class-ctor]] === Instantiation with a Constructor @@ -254,7 +264,6 @@ NOTE: In the case of constructor arguments, the container can select a correspon 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 @@ -284,7 +293,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(); @@ -298,7 +307,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 { @@ -332,7 +341,6 @@ overloads of the `mock` method. Choose the most specific variant of `mock` possi ---- ==== - [[beans-factory-class-instance-factory-method]] === Instantiation by Using an Instance Factory Method @@ -364,7 +372,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 { @@ -378,7 +386,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class DefaultServiceLocator { companion object { @@ -414,7 +422,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 { @@ -434,7 +442,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class DefaultServiceLocator { companion object { @@ -464,7 +472,6 @@ xref:core/beans/definition.adoc#beans-factory-class-static-factory-method[static `FactoryBean` (notice the capitalization) refers to a Spring-specific xref:core/beans/factory-extension.adoc#beans-factory-extension-factorybean[`FactoryBean`] implementation class. - [[beans-factory-type-determination]] === Determining a Bean's Runtime Type @@ -480,5 +487,3 @@ The recommended way to find out about the actual runtime type of a particular be a `BeanFactory.getType` call for the specified bean name. This takes all of the above cases into account and returns the type of object that a `BeanFactory.getBean` call is going to return for the same bean name. - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies.adoc index e22058a1ff3b..e8dd916197ff 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies.adoc @@ -7,6 +7,3 @@ Spring parlance). Even the simplest application has a few objects that work toge present what the end-user sees as a coherent application. This next section explains how you go from defining a number of bean definitions that stand alone to a fully realized application where objects collaborate to achieve a goal. - - - 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..99831396b6e6 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 @@ -16,7 +16,8 @@ advantages: during development, without negating the option of switching to explicit wiring when the code base becomes more stable. -When using XML-based configuration metadata (see xref:core/beans/dependencies/factory-collaborators.adoc[Dependency Injection]), you +When using XML-based configuration metadata (see +xref:core/beans/dependencies/factory-collaborators.adoc[Dependency Injection]), you can specify the autowire mode for a bean definition with the `autowire` attribute of the `` element. The autowiring functionality has four modes. You specify autowiring per bean and can thus choose which ones to autowire. The following table describes the @@ -89,22 +90,22 @@ In the latter scenario, you have several options: * Abandon autowiring in favor of explicit wiring. * Avoid autowiring for a bean definition by setting its `autowire-candidate` attributes - to `false`, as described in the xref:core/beans/dependencies/factory-autowire.adoc#beans-factory-autowire-candidate[next section]. + to `false`, as described in the + xref:core/beans/dependencies/factory-autowire.adoc#beans-factory-autowire-candidate[next section]. * Designate a single bean definition as the primary candidate by setting the `primary` attribute of its `` element to `true`. * Implement the more fine-grained control available with annotation-based configuration, as described in xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration]. - [[beans-factory-autowire-candidate]] == 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,22 @@ 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..405123d5018f 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 @@ -16,8 +16,9 @@ not know the location or class of the dependencies. As a result, your classes be to test, particularly when the dependencies are on interfaces or abstract base classes, which allow for stub or mock implementations to be used in unit tests. -DI exists in two major variants: xref:core/beans/dependencies/factory-collaborators.adoc#beans-constructor-injection[Constructor-based dependency injection] - and xref:core/beans/dependencies/factory-collaborators.adoc#beans-setter-injection[Setter-based dependency injection]. +DI exists in two major variants: +xref:core/beans/dependencies/factory-collaborators.adoc#beans-constructor-injection[Constructor-based dependency injection] +and xref:core/beans/dependencies/factory-collaborators.adoc#beans-setter-injection[Setter-based dependency injection]. [[beans-constructor-injection]] @@ -34,7 +35,7 @@ injection: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class SimpleMovieLister { @@ -52,7 +53,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 +78,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 +92,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 +128,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 +149,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 +160,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 +175,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 +195,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 +212,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 +222,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 +240,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 +249,6 @@ Kotlin:: constructor(val years: Int, val ultimateAnswer: String) ---- ====== --- [[beans-setter-injection]] @@ -262,7 +266,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 +284,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SimpleMovieLister { @@ -437,7 +441,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 +467,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 +504,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 +525,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 +558,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 +582,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 { @@ -603,6 +607,3 @@ contains the `static` factory method (although, in this example, it is). An inst (non-static) factory method can be used in an essentially identical fashion (aside from the use of the `factory-bean` attribute instead of the `class` attribute), so we do not discuss those details here. - - - 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..95e2be3b662c 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,7 @@ 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 353cf0ae6e1e..0cc761249156 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 @@ -29,6 +29,3 @@ annotated class or in XML using the `default-lazy-init` attribute on the ` - + @@ -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,22 +291,17 @@ 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`/ -`Provider` injection point. See xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies]. +`Provider` injection point. See +xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies]. You may also find the `ServiceLocatorFactoryBean` (in the `org.springframework.beans.factory.config` package) to be useful. ==== - [[beans-factory-arbitrary-method-replacement]] == Arbitrary Method Replacement @@ -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) @@ -429,6 +422,3 @@ substring of the fully qualified type name. For example, the following all match Because the number of arguments is often enough to distinguish between each possible choice, this shortcut can save a lot of typing, by letting you type only the shortest string that matches an argument type. - - - 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 a31499d5e267..904f037575ea 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 @@ -1,11 +1,10 @@ [[beans-factory-properties-detailed]] = Dependencies and Configuration in Detail -As mentioned in the xref:core/beans/dependencies/factory-collaborators.adoc[previous section], you can define bean -properties and constructor arguments as references to other managed beans (collaborators) -or as values defined inline. Spring's XML-based configuration metadata supports -sub-element types within its `` and `` elements for this -purpose. +As mentioned in the xref:core/beans/dependencies/factory-collaborators.adoc[previous section], +you can define bean properties and constructor arguments as references to other managed beans +(collaborators) or as values defined inline. Spring's XML-based configuration metadata supports +sub-element types within its `` and `` elements for this purpose. [[beans-value-element]] @@ -51,9 +50,8 @@ 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 {spring-site-tools}[Spring Tools for Eclipse]) -that supports automatic property completion when you create bean definitions. Such IDE -assistance is highly recommended. +IDEA] or the {spring-site-tools}[Spring Tools]) that supports automatic property +completion when you create bean definitions. Such IDE assistance is highly recommended. You can also configure a `java.util.Properties` instance, as follows: @@ -86,11 +84,11 @@ element. The following example shows how to use it: [source,xml,indent=0,subs="verbatim,quotes"] ---- - + - + - + ---- @@ -100,28 +98,24 @@ following snippet: [source,xml,indent=0,subs="verbatim,quotes"] ---- - + - + ---- The first form is preferable to the second, because using the `idref` tag lets the -container validate at deployment time that the referenced, named bean actually -exists. In the second variation, no validation is performed on the value that is passed -to the `targetName` property of the `client` bean. Typos are only discovered (with most +container validate at deployment time that the referenced, named bean actually exists. In +the second variation, no validation is performed on the value that is passed to the +`targetName` property of the `client` bean. Typos are therefore only discovered (with most likely fatal results) when the `client` bean is actually instantiated. If the `client` -bean is a xref:core/beans/factory-scopes.adoc[prototype] bean, this typo and the resulting exception -may only be discovered long after the container is deployed. - -NOTE: The `local` attribute on the `idref` element is no longer supported in the 4.0 beans -XSD, since it does not provide value over a regular `bean` reference any more. Change -your existing `idref local` references to `idref bean` when upgrading to the 4.0 schema. +bean is a xref:core/beans/factory-scopes.adoc[prototype] bean, this typo and the resulting +exception may only be discovered long after the container is deployed. -A common place (at least in versions earlier than Spring 2.0) where the `` element -brings value is in the configuration of xref:core/aop-api/pfb.adoc#aop-pfb-1[AOP interceptors] in a -`ProxyFactoryBean` bean definition. Using `` elements when you specify the +NOTE: A common place (at least in versions earlier than Spring 2.0) where the `` +element brings value is in the configuration of xref:core/aop-api/pfb.adoc#aop-pfb-1[AOP interceptors] +in a `ProxyFactoryBean` bean definition. Using `` elements when you specify the interceptor names prevents you from misspelling an interceptor ID. @@ -168,8 +162,8 @@ listings shows how to use the `parent` attribute: [source,xml,indent=0,subs="verbatim,quotes"] ---- - - + + @@ -354,7 +348,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 +362,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 +412,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 +443,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 ---- @@ -544,9 +538,10 @@ three approaches at the same time. [[beans-c-namespace]] == XML Shortcut with the c-namespace -Similar to the xref:core/beans/dependencies/factory-properties-detailed.adoc#beans-p-namespace[XML Shortcut with the p-namespace], the c-namespace, introduced in Spring -3.1, allows inlined attributes for configuring the constructor arguments rather -then nested `constructor-arg` elements. +Similar to the +xref:core/beans/dependencies/factory-properties-detailed.adoc#beans-p-namespace[XML Shortcut with the p-namespace], +the c-namespace, introduced in Spring 3.1, allows inlined attributes for configuring +the constructor arguments rather then nested `constructor-arg` elements. The following example uses the `c:` namespace to do the same thing as the from xref:core/beans/dependencies/factory-collaborators.adoc#beans-constructor-injection[Constructor-based Dependency Injection]: @@ -582,7 +577,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"] @@ -598,9 +593,9 @@ A corresponding index notation is also available for `` element not commonly used since the plain order of declaration is usually sufficient there. In practice, the constructor resolution -xref:core/beans/dependencies/factory-collaborators.adoc#beans-factory-ctor-arguments-resolution[mechanism] is quite efficient in matching -arguments, so unless you really need to, we recommend using the name notation -throughout your configuration. +xref:core/beans/dependencies/factory-collaborators.adoc#beans-factory-ctor-arguments-resolution[mechanism] +is quite efficient in matching arguments, so unless you really need to, we recommend +using the name notation throughout your configuration. [[beans-compound-property-names]] @@ -621,6 +616,3 @@ The `something` bean has a `fred` property, which has a `bob` property, which ha property, and that final `sammy` property is being set to a value of `123`. In order for this to work, the `fred` property of `something` and the `bob` property of `fred` must not be `null` after the bean is constructed. Otherwise, a `NullPointerException` is thrown. - - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/environment.adoc b/framework-docs/modules/ROOT/pages/core/beans/environment.adoc index ac8085f0e287..883fcaeff87b 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/environment.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/environment.adoc @@ -20,7 +20,6 @@ user with a convenient service interface for configuring property sources and re properties from them. - [[beans-definition-profiles]] == Bean Definition Profiles @@ -43,7 +42,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 +56,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 +78,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 +89,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean(destroyMethod = "") fun dataSource(): DataSource { @@ -114,7 +113,6 @@ certain contexts but not in others. You could say that you want to register a certain profile of bean definitions in situation A and a different profile in situation B. We start by updating our configuration to reflect this need. - [[beans-definition-profiles-java]] === Using `@Profile` @@ -128,7 +126,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 +145,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("development") @@ -171,7 +169,7 @@ Kotlin:: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("production") @@ -188,7 +186,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("production") @@ -233,7 +231,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 +242,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 +270,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 +298,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -345,7 +343,6 @@ way to represent such an arrangement in a valid Java class in the first place (since there can only be one method of a particular name and argument signature). ==== - [[beans-definition-profiles-xml]] === XML Bean Definition Profiles @@ -437,7 +434,6 @@ In the preceding example, the `dataSource` bean is exposed if both the `producti `us-east` profiles are active. ===== - [[beans-definition-profiles-enable]] === Activating a Profile @@ -454,7 +450,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 +460,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 +487,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") ---- @@ -512,7 +508,6 @@ as the following example shows: -Dspring.profiles.active="profile1,profile2" ---- - [[beans-definition-profiles-default]] === Default Profile @@ -523,7 +518,7 @@ the following example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("default") @@ -541,7 +536,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @Profile("default") @@ -567,7 +562,6 @@ the default profile by using `setDefaultProfiles()` on the `Environment` or, declaratively, by using the `spring.profiles.default` property. - [[beans-property-source-abstraction]] == `PropertySource` Abstraction @@ -578,7 +572,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 +582,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 @@ -643,7 +637,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 +646,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 @@ -668,7 +662,6 @@ API exposes a number of methods that allow for precise manipulation of the set o property sources. - [[beans-using-propertysource]] == Using `@PropertySource` @@ -684,7 +677,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 +697,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 +722,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 +742,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") @@ -777,7 +770,6 @@ may also be used as a meta-annotation to create custom composed annotations with attribute overrides. - [[beans-placeholder-resolution-in-statements]] == Placeholder Resolution in Statements @@ -798,7 +790,3 @@ property is defined, as long as it is available in the `Environment`: ---- - - - - 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 86d3a9911157..25f14531780a 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc @@ -7,7 +7,6 @@ implementations of special integration interfaces. The next few sections describ integration interfaces. - [[beans-factory-extension-bpp]] == Customizing Beans by Using a `BeanPostProcessor` @@ -68,6 +67,13 @@ interface, clearly indicating the post-processor nature of that bean. Otherwise, Since a `BeanPostProcessor` needs to be instantiated early in order to apply to the initialization of other beans in the context, this early type detection is critical. +Furthermore, when registering a `BeanPostProcessor` via an `@Bean` factory method, +declare the method as `static` and ideally with no dependencies. Doing so avoids eager +initialization of the configuration class and other beans, which would make them +ineligible for full post-processing (such as auto-proxying). See the +"BeanPostProcessor-returning `@Bean` methods" section in the +{spring-framework-api}/context/annotation/Bean.html[`@Bean`] javadoc for details. + [[beans-factory-programmatically-registering-beanpostprocessors]] .Programmatically registering `BeanPostProcessor` instances NOTE: While the recommended approach for `BeanPostProcessor` registration is through @@ -81,7 +87,7 @@ of execution. Note also that `BeanPostProcessor` instances registered programmat are always processed before those registered through auto-detection, regardless of any explicit ordering. -.`BeanPostProcessor` instances and AOP auto-proxying +.`BeanPostProcessor` instances and early initialization [NOTE] ==== Classes that implement the `BeanPostProcessor` interface are special and are treated @@ -91,17 +97,23 @@ of the `ApplicationContext`. Next, all `BeanPostProcessor` instances are registe in a sorted fashion and applied to all further beans in the container. Because AOP auto-proxying is implemented as a `BeanPostProcessor` itself, neither `BeanPostProcessor` instances nor the beans they directly reference are eligible for auto-proxying and, -thus, do not have aspects woven into them. - -For any such bean, you should see an informational log message: `Bean someBean is not -eligible for getting processed by all BeanPostProcessor interfaces (for example: not -eligible for auto-proxying)`. - -If you have beans wired into your `BeanPostProcessor` by using autowiring or -`@Resource` (which may fall back to autowiring), Spring might access unexpected beans -when searching for type-matching dependency candidates and, therefore, make them -ineligible for auto-proxying or other kinds of bean post-processing. For example, if you -have a dependency annotated with `@Resource` where the field or setter name does not +thus, do not have aspects woven into them. More generally, any bean that is instantiated +during this early phase is not eligible for full post-processing by all +`BeanPostProcessor` instances. + +For any such bean, you should see a WARN-level log message similar to the following. + +[quote] +Bean 'someBean' of type [org.example.SomeType] is not eligible for getting processed by +all BeanPostProcessors (for example: not eligible for auto-proxying). + +To minimize the number of beans affected, register a `BeanPostProcessor` with a +`static` `@Bean` method that has no dependencies (see the note above). If you have +beans wired into your `BeanPostProcessor` by using autowiring or `@Resource` (which +may fall back to autowiring), Spring might access unexpected beans when searching +for type-matching dependency candidates and, therefore, make them ineligible for +auto-proxying or other kinds of bean post-processing. For example, if you have a +dependency annotated with `@Resource` where the field or setter name does not directly correspond to the declared name of a bean and no name attribute is used, Spring accesses other beans for matching them by type. ==== @@ -109,7 +121,6 @@ Spring accesses other beans for matching them by type. The following examples show how to write, register, and use `BeanPostProcessor` instances in an `ApplicationContext`. - [[beans-factory-extension-bpp-examples-hw]] === Example: Hello World, `BeanPostProcessor`-style @@ -123,7 +134,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; @@ -137,7 +148,7 @@ Java:: } public Object postProcessAfterInitialization(Object bean, String beanName) { - System.out.println("Bean '" + beanName + "' created : " + bean.toString()); + System.out.println("Bean '" + beanName + "' created : " + bean); return bean; } } @@ -145,7 +156,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 @@ -166,7 +177,48 @@ Kotlin:: ---- ====== -The following `beans` element uses the `InstantiationTracingBeanPostProcessor`: +You can register the `InstantiationTracingBeanPostProcessor` with Java configuration +by using a `static` `@Bean` method (recommended to avoid eager initialization of the +configuration class and other beans): + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] +---- + @Configuration + public class AppConfig { + + @Bean + public static InstantiationTracingBeanPostProcessor instantiationTracingBeanPostProcessor() { + return new InstantiationTracingBeanPostProcessor(); + } + + // ... other bean definitions + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] +---- + @Configuration + class AppConfig { + + @Bean + companion object { + @JvmStatic + fun instantiationTracingBeanPostProcessor() = InstantiationTracingBeanPostProcessor() + } + + // ... other bean definitions + } +---- +====== + +Alternatively, the `InstantiationTracingBeanPostProcessor` can be registered via the +`bean` element with XML configuration: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -194,10 +246,9 @@ The following `beans` element uses the `InstantiationTracingBeanPostProcessor`: ---- Notice how the `InstantiationTracingBeanPostProcessor` is merely defined. It does not -even have a name, and, because it is a bean, it can be dependency-injected as you would any +even have a name, and, because it is a bean, it can be dependency-injected as with any other bean. (The preceding configuration also defines a bean that is backed by a Groovy -script. The Spring dynamic language support is detailed in the chapter entitled -xref:languages/dynamic.adoc[Dynamic Language Support].) +script.) The following Java application runs the preceding code and configuration: @@ -205,7 +256,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,9 +275,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - import org.springframework.beans.factory.getBean + import org.springframework.beans.factory.getBean fun main() { val ctx = ClassPathXmlApplicationContext("scripting/beans.xml") @@ -244,7 +295,6 @@ Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@ org.springframework.scripting.groovy.GroovyMessenger@272961 ---- - [[beans-factory-extension-bpp-examples-aabpp]] === Example: The `AutowiredAnnotationBeanPostProcessor` @@ -255,7 +305,6 @@ that ships with the Spring distribution and autowires annotated fields, setter m and arbitrary config methods. - [[beans-factory-extension-factory-postprocessors]] == Customizing Configuration Metadata with a `BeanFactoryPostProcessor` @@ -305,6 +354,23 @@ implement the `BeanFactoryPostProcessor` interface. It uses these beans as bean post-processors, at the appropriate time. You can deploy these post-processor beans as you would any other bean. +When registering a `BeanFactoryPostProcessor` via an `@Bean` factory method in a +`@Configuration` class, declare the method as `static` to avoid lifecycle conflicts +with annotation processing (such as `@Autowired`, `@Value`, and `@PostConstruct`) in the +configuration class. See the "BeanFactoryPostProcessor-returning `@Bean` methods" +section in the {spring-framework-api}/context/annotation/Bean.html[`@Bean`] javadoc +for details and an example. + +For any non-static `@Bean` factory method with a `BeanFactoryPostProcessor` return type, +you should see an INFO-level log message similar to the following. + +[quote] +@Bean method MyConfig.myBfpp is non-static and returns an object assignable to Spring's +BeanFactoryPostProcessor interface. This will result in a failure to process annotations +such as @Autowired, @Resource, and @PostConstruct within the method's declaring +@Configuration class. Add the 'static' modifier to this method to avoid these container +lifecycle issues; see @Bean javadoc for complete details. + NOTE: As with ``BeanPostProcessor``s , you typically do not want to configure ``BeanFactoryPostProcessor``s for lazy initialization. If no other bean references a `Bean(Factory)PostProcessor`, that post-processor will not get instantiated at all. @@ -312,9 +378,8 @@ Thus, marking it for lazy initialization will be ignored, and the `Bean(Factory)PostProcessor` will be instantiated eagerly even if you set the `default-lazy-init` attribute to `true` on the declaration of your `` element. - [[beans-factory-placeholderconfigurer]] -=== Example: The Class Name Substitution `PropertySourcesPlaceholderConfigurer` +=== Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer` You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values from a bean definition in a separate file by using the standard Java `Properties` format. @@ -341,8 +406,8 @@ with placeholder values is defined: The example shows properties configured from an external `Properties` file. At runtime, a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some -properties of the DataSource. The values to replace are specified as placeholders of the -form pass:q[`${property-name}`], which follows the Ant and log4j and JSP EL style. +properties of the `DataSource`. The values to replace are specified as placeholders of the +form pass:q[`${property-name}`], which follows the Ant, log4j, and JSP EL style. The actual values come from another file in the standard Java `Properties` format: @@ -355,11 +420,15 @@ jdbc.password=root ---- Therefore, the `${jdbc.username}` string is replaced at runtime with the value, 'sa', and -the same applies for other placeholder values that match keys in the properties file. -The `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and -attributes of a bean definition. Furthermore, you can customize the placeholder prefix and suffix. - -With the `context` namespace introduced in Spring 2.5, you can configure property placeholders +the same applies for other placeholder values that match keys in the properties file. The +`PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and +attributes of a bean definition. Furthermore, you can customize the placeholder prefix, +suffix, default value separator, and escape character. In addition, the default escape +character can be changed or disabled globally by setting the +`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via +the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism). + +With the `context` namespace, you can configure property placeholders with a dedicated configuration element. You can provide one or more locations as a comma-separated list in the `location` attribute, as the following example shows: @@ -408,7 +477,6 @@ fails when it is about to be created, which is during the `preInstantiateSinglet phase of an `ApplicationContext` for a non-lazy-init bean. ===== - [[beans-factory-overrideconfigurer]] === Example: The `PropertyOverrideConfigurer` @@ -439,7 +507,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 @@ -465,7 +533,6 @@ property overriding with a dedicated configuration element, as the following exa ---- - [[beans-factory-extension-factorybean]] == Customizing Instantiation Logic with a `FactoryBean` @@ -498,6 +565,3 @@ calling the `getBean()` method of the `ApplicationContext`. So, for a given `Fac with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the `FactoryBean` instance itself. - - - 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 7ed7cd011825..87d1cefe7491 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-nature.adoc @@ -9,7 +9,6 @@ of a bean. This section groups them as follows: * xref:core/beans/factory-nature.adoc#aware-list[Other `Aware` Interfaces] - [[beans-factory-lifecycle]] == Lifecycle Callbacks @@ -41,8 +40,6 @@ startup and shutdown process, as driven by the container's own lifecycle. The lifecycle callback interfaces are described in this section. - - [[beans-factory-lifecycle-initializingbean]] === Initialization Callbacks @@ -72,7 +69,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 +81,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleBean { @@ -107,7 +104,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 +117,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class AnotherExampleBean : InitializingBean { @@ -144,7 +141,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)`. @@ -156,8 +153,6 @@ the container's overall lifecycle management, including an auto-startup mechanis a pre-destroy stop step, and potential stop/restart callbacks (see below). ==== - - [[beans-factory-lifecycle-disposablebean]] === Destruction Callbacks @@ -187,7 +182,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 +194,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleBean { @@ -221,7 +216,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 +229,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class AnotherExampleBean : DisposableBean { @@ -267,8 +262,6 @@ You may also implement `SmartLifecycle` for a time-bound stop step where the con will wait for all such stop processing to complete before moving on to destroy methods. ==== - - [[beans-factory-lifecycle-default-init-destroy-methods]] === Default Initialization and Destroy Methods @@ -295,7 +288,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 +309,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class DefaultBlogService : BlogService { @@ -369,8 +362,6 @@ interceptors to the `init` method, because doing so would couple the lifecycle o target bean to its proxy or interceptors and leave strange semantics when your code interacts directly with the raw target bean. - - [[beans-factory-lifecycle-combined-effects]] === Combining Lifecycle Mechanisms @@ -402,8 +393,6 @@ Destroy methods are called in the same order: . `destroy()` as defined by the `DisposableBean` callback interface . A custom configured `destroy()` method - - [[beans-factory-lifecycle-processor]] === Startup and Shutdown Callbacks @@ -526,8 +515,6 @@ its own `start()` method (unlike the context refresh, the context start does not automatically for a standard context implementation). The `phase` value and any "`depends-on`" relationships determine the startup order as described earlier. - - [[beans-factory-shutdown]] === Shutting Down the Spring IoC Container Gracefully in Non-Web Applications @@ -551,7 +538,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 +560,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 @@ -590,8 +577,6 @@ Kotlin:: ---- ====== - - [[beans-factory-thread-safety]] === Thread Safety and Visibility @@ -607,7 +592,7 @@ bean creation phase and its subsequent initial publication, they need to be decl `volatile` or guarded by a common lock whenever accessed. Note that concurrent access to such configuration state in singleton bean instances, -e.g. for controller instances or repository instances, is perfectly thread-safe after +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. @@ -617,7 +602,7 @@ structures (or in `volatile` fields for simple cases) as per common Java guideli 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, e.g. a start callback is guaranteed to +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 @@ -625,7 +610,6 @@ destroy callback without a preceding stop since this may happen during an extrao shutdown after a cancelled bootstrap or in case of a stop timeout caused by another bean. - [[beans-factory-aware]] == `ApplicationContextAware` and `BeanNameAware` @@ -682,7 +666,6 @@ initialization callback such as `InitializingBean.afterPropertiesSet()` or a cus init-method. - [[aware-list]] == Other `Aware` Interfaces @@ -747,6 +730,3 @@ dependency type. The following table summarizes the most important `Aware` inter Note again that using these interfaces ties your code to the Spring API and does not follow the Inversion of Control style. As a result, we recommend them for infrastructure beans that require programmatic access to the container. - - - 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 6049003235d4..d5317a4d46fe 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc @@ -56,7 +56,6 @@ 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]. - [[beans-factory-scopes-singleton]] == The Singleton Scope @@ -91,7 +90,6 @@ following example: ---- - [[beans-factory-scopes-prototype]] == The Prototype Scope @@ -134,7 +132,6 @@ be handled by the client. (For details on the lifecycle of a bean in the Spring container, see xref:core/beans/factory-nature.adoc#beans-factory-lifecycle[Lifecycle Callbacks].) - [[beans-factory-scopes-sing-prot-interaction]] == Singleton Beans with Prototype-bean Dependencies @@ -152,7 +149,6 @@ and injects its dependencies. If you need a new instance of a prototype bean at runtime more than once, see xref:core/beans/dependencies/factory-method-injection.adoc[Method Injection]. - [[beans-factory-scopes-other]] == Request, Session, Application, and WebSocket Scopes @@ -162,8 +158,6 @@ if you use a web-aware Spring `ApplicationContext` implementation (such as such as the `ClassPathXmlApplicationContext`, an `IllegalStateException` that complains about an unknown bean scope is thrown. - - [[beans-factory-scopes-other-web-configuration]] === Initial Web Configuration @@ -223,8 +217,6 @@ the same thing, namely bind the HTTP request object to the `Thread` that is serv that request. This makes beans that are request- and session-scoped available further down the call chain. - - [[beans-factory-scopes-request]] === Request scope @@ -251,7 +243,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 +254,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RequestScope @Component @@ -272,8 +264,6 @@ Kotlin:: ---- ====== - - [[beans-factory-scopes-session]] === Session Scope @@ -301,7 +291,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 +302,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SessionScope @Component @@ -322,8 +312,6 @@ Kotlin:: ---- ====== - - [[beans-factory-scopes-application]] === Application Scope @@ -350,7 +338,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 @@ -361,7 +349,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ApplicationScope @Component @@ -371,8 +359,6 @@ Kotlin:: ---- ====== - - [[beans-factory-scopes-websocket]] === WebSocket Scope @@ -380,8 +366,6 @@ WebSocket scope is associated with the lifecycle of a WebSocket session and appl STOMP over WebSocket applications, see xref:web/websocket/stomp/scope.adoc[WebSocket scope] for more details. - - [[beans-factory-scopes-other-injection]] === Scoped Beans as Dependencies @@ -539,8 +523,6 @@ interfaces. The following example shows a proxy based on an interface: For more detailed information about choosing class-based or interface-based proxying, see xref:core/aop/proxying.adoc[Proxying Mechanisms]. - - [[beans-factory-scopes-injection]] === Injecting Request/Session References Directly @@ -553,7 +535,6 @@ objects which has the advantage of working in singleton beans and serializable b as well, similar to scoped proxies for factory-scoped beans. - [[beans-factory-scopes-custom]] == Custom Scopes @@ -561,7 +542,6 @@ The bean scoping mechanism is extensible. You can define your own scopes or even redefine existing scopes, although the latter is considered bad practice and you cannot override the built-in `singleton` and `prototype` scopes. - [[beans-factory-scopes-custom-creating]] === Creating a Custom Scope @@ -584,14 +564,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 ---- @@ -606,14 +586,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 ---- @@ -626,14 +606,14 @@ 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) ---- @@ -648,14 +628,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 ---- @@ -664,8 +644,6 @@ Kotlin:: This identifier is different for each scope. For a session scoped implementation, this identifier can be the session identifier. - - [[beans-factory-scopes-custom-using]] === Using a Custom Scope @@ -677,14 +655,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) ---- @@ -710,7 +688,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); @@ -718,7 +696,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) @@ -773,7 +751,3 @@ of the scope. You can also do the `Scope` registration declaratively, by using t NOTE: When you place `` within a `` declaration for a `FactoryBean` implementation, it is the factory bean itself that is scoped, not the object returned from `getObject()`. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc b/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc index 969cbed145f6..c4728cfff562 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/introduction.adoc @@ -37,7 +37,3 @@ by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and managed by a Spring IoC container. Otherwise, a bean is simply one of many objects in your application. Beans, and the dependencies among them, are reflected in the configuration metadata used by a container. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/beans/java.adoc b/framework-docs/modules/ROOT/pages/core/beans/java.adoc index 8f3f9f7aac0b..86ca51e37219 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java.adoc @@ -4,4 +4,3 @@ This section covers how to use annotations in your Java code to configure the Spring 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 5b9277838956..adc615e35ea9 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 { @@ -64,7 +64,7 @@ This prevents the same `@Bean` method from accidentally being invoked through a 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 -, +`@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). @@ -84,6 +84,3 @@ subclassing has to be applied at runtime, reducing the overhead and the footprin 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 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 4e089707ac84..1437b657f5a1 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 @@ -4,10 +4,10 @@ `@Bean` is a method-level annotation and a direct analog of the XML `` element. The annotation supports some of the attributes offered by ``, such as: +* xref:core/beans/definition.adoc#beans-beanname[name] * xref:core/beans/factory-nature.adoc#beans-factory-lifecycle-initializingbean[init-method] * xref:core/beans/factory-nature.adoc#beans-factory-lifecycle-disposablebean[destroy-method] * xref:core/beans/dependencies/factory-autowire.adoc[autowiring] -* `name`. You can use the `@Bean` annotation in a `@Configuration`-annotated or in a `@Component`-annotated class. @@ -17,15 +17,16 @@ You can use the `@Bean` annotation in a `@Configuration`-annotated or in a == Declaring a Bean To declare a bean, you can annotate a method with the `@Bean` annotation. You use this -method to register a bean definition within an `ApplicationContext` of the type -specified as the method's return value. By default, the bean name is the same as -the method name. The following example shows a `@Bean` method declaration: +method to register a bean definition within an `ApplicationContext` of the type specified +by the method's return type. By default, the bean name is the same as the method name +(unless a different xref:#beans-java-customizing-bean-naming[bean name generator] is +configured). The following example shows a `@Bean` method declaration: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class AppConfig { @@ -39,7 +40,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -75,7 +76,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 +100,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 +114,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -126,7 +127,7 @@ Kotlin:: ---- ====== -However, this limits the visibility for advance type prediction to the specified +However, this limits the visibility for advanced type prediction to the specified interface type (`TransferService`). Then, with the full type (`TransferServiceImpl`) known to the container only once the affected singleton bean has been instantiated. Non-lazy singleton beans get instantiated according to their declaration order, @@ -153,7 +154,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 +168,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -211,7 +212,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 +245,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class BeanOne { @@ -291,7 +292,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 +302,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 +327,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 +345,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -382,7 +383,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 +398,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class MyConfiguration { @@ -431,7 +432,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 +452,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 @@ -468,18 +469,26 @@ Kotlin:: ---- ====== + [[beans-java-customizing-bean-naming]] == Customizing Bean Naming By default, configuration classes use a `@Bean` method's name as the name of the -resulting bean. This functionality can be overridden, however, with the `name` attribute, -as the following example shows: +resulting bean. However, as of Spring Framework 7.0, you can change this default strategy +by configuring a custom +{spring-framework-api}/context/annotation/ConfigurationBeanNameGenerator.html[`ConfigurationBeanNameGenerator`] +when bootstrapping the context or configuring component scanning. For example, +{spring-framework-api}/context/annotation/FullyQualifiedConfigurationBeanNameGenerator.html[`FullyQualifiedConfigurationBeanNameGenerator`] +can be used to generate fully-qualified default bean names for `@Bean` methods without an +explicit `name` attribute. For individual `@Bean` methods, the default or +generator-derived name can be overridden with the `name` attribute, 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 { @@ -493,7 +502,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -504,6 +513,7 @@ Kotlin:: ---- ====== +NOTE: `@Bean("myThing")` is equivalent to `@Bean(name = "myThing")`. [[beans-java-bean-aliasing]] == Bean Aliasing @@ -517,7 +527,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 +541,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -559,7 +569,7 @@ annotation, 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 { @@ -574,7 +584,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -585,6 +595,3 @@ Kotlin:: } ---- ====== - - - 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 edc8af846c90..3bd4eaf272e1 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,7 +219,7 @@ 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 @@ -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 @@ -327,21 +327,22 @@ Kotlin:: ---- ====== -TIP: Constructor injection in `@Configuration` classes is only supported as of Spring -Framework 4.3. Note also that there is no need to specify `@Autowired` if the target -bean defines only one constructor. +TIP: Note that there is no need to specify `@Autowired` if the target bean defines +only one constructor. + +[discrete] +[[beans-java-injecting-imported-beans-fq]] +==== Fully-qualifying imported beans for ease of navigation -.[[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 -{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. +explicit in the code, and this may be just fine. Note that the +{spring-site-tools}[Spring Tools] IDE support 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. In cases where this ambiguity is not acceptable and you wish to have direct navigation from within your IDE from one `@Configuration` class to another, consider autowiring the @@ -351,7 +352,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 +370,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class ServiceConfig { @@ -395,7 +396,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 +446,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,7 +502,6 @@ 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. --- [[beans-java-startup]] @@ -569,7 +569,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) { @@ -577,7 +577,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; } } @@ -589,14 +589,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 } } @@ -610,6 +610,19 @@ Kotlin:: See the {spring-framework-api}/context/annotation/Conditional.html[`@Conditional`] javadoc for more detail. +[NOTE] +==== +A `@Conditional` annotation declared on an enclosing `@Configuration` class is only +applied to the registration of a nested `@Configuration` class if the nested class is +reached through the parser's recursion from its enclosing class, or via `@Import`. If a +nested class is discovered independently of its enclosing class — for example, via +`@ComponentScan` or by directly registering it against the application context — it is +processed using only its own `@Conditional` annotations. Thus, if you wish to ensure that +the same `@Conditional` annotations apply in such scenarios, you must redeclare the +relevant annotations on the nested class, or extract them into a composed annotation +which you apply to both the enclosing class and the nested class. +==== + [[beans-java-combining]] == Combining Java and XML Configuration @@ -631,22 +644,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 { @@ -661,14 +676,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 { @@ -694,6 +709,7 @@ The following example shows part of a sample `system-test-config.xml` file: + @@ -719,7 +735,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"); @@ -730,7 +746,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") @@ -740,20 +756,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. @@ -764,6 +780,7 @@ The following example shows the modified `system-test-config.xml` file: + @@ -773,25 +790,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") @@ -810,12 +825,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") @@ -834,21 +860,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= @@ -858,7 +895,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); @@ -869,7 +906,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 @@ -880,6 +917,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..2e17543405b0 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 @@ -4,7 +4,9 @@ `@Configuration` is a class-level annotation indicating that an object is a source of bean definitions. `@Configuration` classes declare beans through `@Bean`-annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define -inter-bean dependencies. See xref:core/beans/java/basic-concepts.adoc[Basic Concepts: `@Bean` and `@Configuration`] for a general introduction. +inter-bean dependencies. See +xref:core/beans/java/basic-concepts.adoc[Basic Concepts: `@Bean` and `@Configuration`] +for a general introduction. [[beans-java-injecting-dependencies]] @@ -17,7 +19,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 +38,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -58,7 +60,6 @@ is declared within a `@Configuration` class. You cannot declare inter-bean depen by using plain `@Component` classes. - [[beans-java-method-injection]] == Lookup Method Injection @@ -72,7 +73,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 +91,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 +116,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 +140,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean @Scope("prototype") @@ -172,7 +173,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 +201,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class AppConfig { @@ -259,6 +260,3 @@ instead) or by annotating your configuration class with are then not intercepted, so you have to exclusively rely on dependency injection at the constructor or method level there. ==== - - - 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 137bfe28398b..87cb5f91d680 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() @@ -206,7 +206,8 @@ Kotlin:: ---- ====== -NOTE: Remember that `@Configuration` classes are xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotated] +NOTE: Remember that `@Configuration` classes are +xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotated] with `@Component`, so they are candidates for component-scanning. In the preceding example, assuming that `AppConfig` is declared within the `com.acme` package (or any package underneath), it is picked up during the call to `scan()`. Upon `refresh()`, all its `@Bean` @@ -280,5 +281,3 @@ NOTE: For programmatic use cases, a `GenericWebApplicationContext` can be used a alternative to `AnnotationConfigWebApplicationContext`. See the {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..3663fc13fe77 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/core/beans/java/programmatic-bean-registration.adoc @@ -0,0 +1,25 @@ +[[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. + +include-code::./MyConfiguration[tag=snippet,indent=0] + +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. + +include-code::./MyBeanRegistrar[tag=snippet,indent=0] + +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..dfb9515fafec 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/standard-annotations.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/standard-annotations.adoc @@ -1,16 +1,17 @@ [[beans-standard-annotations]] -= Using JSR 330 Standard Annotations += Using JSR-330 Standard Annotations -Spring offers support for JSR-330 standard annotations (Dependency Injection). Those -annotations are scanned in the same way as the Spring annotations. To use them, you need -to have the relevant jars in your classpath. +Spring offers support for JSR-330 standard _Dependency Injection_ annotations which are +available in the `jakarta.inject` package. These annotations may optionally be used as +alternatives to Spring annotations. + +To use them, you need to have the relevant jar in your classpath. For example, the +`jakarta.inject` artifact is available in the standard Maven repository +(`https://repo.maven.apache.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/`), [NOTE] ===== -If you use Maven, the `jakarta.inject` artifact is available in the standard Maven -repository ( -https://repo.maven.apache.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/[https://repo.maven.apache.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/]). -You can add the following dependency to your file pom.xml: +If you use Maven, you can add the following dependency to your `pom.xml` file. [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -23,17 +24,17 @@ You can add the following dependency to your file pom.xml: ===== - [[beans-inject-named]] == Dependency Injection with `@Inject` and `@Named` -Instead of `@Autowired`, you can use `@jakarta.inject.Inject` as follows: +Instead of using `@Autowired` for dependency injection, you may optionally choose to use +`@jakarta.inject.Inject` as follows. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject; @@ -55,7 +56,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject @@ -73,17 +74,19 @@ Kotlin:: ---- ====== -As with `@Autowired`, you can use `@Inject` at the field level, method level -and constructor-argument level. Furthermore, you may declare your injection point as a -`Provider`, allowing for on-demand access to beans of shorter scopes or lazy access to -other beans through a `Provider.get()` call. The following example offers a variant of the -preceding example: +As with `@Autowired`, you can use `@Inject` at the field level, method level, and +constructor-argument level. + +Furthermore, as an alternative to Spring's `ObjectProvider` mechanism, you may choose to +declare your injection point as a `jakarta.inject.Provider`, allowing for on-demand +access to beans of shorter scopes or lazy access to other beans through a +`Provider.get()` call. The following example offers a variant of the preceding example. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject; import jakarta.inject.Provider; @@ -106,9 +109,10 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject + import jakarta.inject.Provider class SimpleMovieLister { @@ -124,14 +128,15 @@ Kotlin:: ---- ====== -If you would like to use a qualified name for the dependency that should be injected, -you should use the `@Named` annotation, as the following example shows: +If you would like to use a qualified name for the dependency that should be injected, you +may choose to use the `@Named` annotation as an alternative to Spring's `@Qualifier` +support, as the following example shows. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject; import jakarta.inject.Named; @@ -151,7 +156,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject import jakarta.inject.Named @@ -171,12 +176,15 @@ Kotlin:: ====== As with `@Autowired`, `@Inject` can also be used with `java.util.Optional` or -`@Nullable`. This is even more applicable here, since `@Inject` does not have -a `required` attribute. The following pair of examples show how to use `@Inject` and -`@Nullable`: +`@Nullable`. This is even more applicable here, since `@Inject` does not have a +`required` attribute. The following examples show how to use `@Inject` with `Optional`, +`@Nullable`, and Kotlin's built-in support for nullable types. -[source,java,indent=0,subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",fold="-imports"] ---- + import jakarta.inject.Inject; + import java.util.Optional; + public class SimpleMovieLister { @Inject @@ -190,8 +198,11 @@ 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",fold="-imports"] ---- + import jakarta.inject.Inject; + import org.jspecify.annotations.Nullable; + public class SimpleMovieLister { @Inject @@ -203,8 +214,10 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"] ---- + import jakarta.inject.Inject + class SimpleMovieLister { @Inject @@ -214,23 +227,22 @@ Kotlin:: ====== - [[beans-named]] -== `@Named` and `@ManagedBean`: Standard Equivalents to the `@Component` Annotation +== `@Named`: Standard Equivalent to the `@Component` Annotation -Instead of `@Component`, you can use `@jakarta.inject.Named` or `jakarta.annotation.ManagedBean`, -as the following example shows: +Instead of `@Component` or other Spring stereotype annotations, you may optionally choose +to use `@jakarta.inject.Named`, as the following example shows. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject; import jakarta.inject.Named; - @Named("movieListener") // @ManagedBean("movieListener") could be used as well + @Named("movieListener") public class SimpleMovieLister { private MovieFinder movieFinder; @@ -246,12 +258,12 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject import jakarta.inject.Named - @Named("movieListener") // @ManagedBean("movieListener") could be used as well + @Named("movieListener") class SimpleMovieLister { @Inject @@ -262,14 +274,15 @@ Kotlin:: ---- ====== -It is very common to use `@Component` without specifying a name for the component. -`@Named` can be used in a similar fashion, as the following example shows: +It is very common to use `@Component` or other Spring stereotype annotations without +specifying an explicit name for the component, and `@Named` can be used in a similar +fashion, as the following example shows. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject; import jakarta.inject.Named; @@ -290,7 +303,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"] ---- import jakarta.inject.Inject import jakarta.inject.Named @@ -306,14 +319,14 @@ Kotlin:: ---- ====== -When you use `@Named` or `@ManagedBean`, you can use component scanning in the -exact same way as when you use Spring annotations, as the following example shows: +When you use `@Named`, you can use component scanning in the exact same way as when you +use Spring annotations, as the following example shows. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes",fold="-imports"] ---- @Configuration @ComponentScan(basePackages = "org.example") @@ -324,7 +337,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"] ---- @Configuration @ComponentScan(basePackages = ["org.example"]) @@ -334,60 +347,107 @@ Kotlin:: ---- ====== -NOTE: In contrast to `@Component`, the JSR-330 `@Named` and the JSR-250 `@ManagedBean` -annotations are not composable. You should use Spring's stereotype model for building -custom component annotations. +NOTE: In contrast to `@Component`, the JSR-330 `@Named` annotation is not composable. You +should use Spring's stereotype model for building custom component annotations. +[TIP] +==== +If you work with legacy systems that still use `@javax.inject.Named` or +`@javax.annotation.ManagedBean` for components (note the `javax` package namespace), you +can explicitly configure component scanning to include those annotation types, as shown +in the following example. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",fold="-imports"] +---- + @Configuration + @ComponentScan( + basePackages = "org.example", + includeFilters = @Filter({ + javax.inject.Named.class, + javax.annotation.ManagedBean.class + }) + ) + public class AppConfig { + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"] +---- + @Configuration + @ComponentScan( + basePackages = ["org.example"], + includeFilters = [Filter([ + javax.inject.Named::class, + javax.annotation.ManagedBean::class + ])] + ) + class AppConfig { + // ... + } +---- +====== + +In addition, if you would like for the `value` attributes in `@javax.inject.Named` and +`@javax.annotation.ManagedBean` to be used as component names, you need to override the +`isStereotypeWithNameValue(...)` method in `AnnotationBeanNameGenerator` to add explicit +support for `javax.annotation.ManagedBean` and `javax.inject.Named` and register your +custom `AnnotationBeanNameGenerator` via the `nameGenerator` attribute in +`@ComponentScan`. +==== [[beans-standard-annotations-limitations]] == Limitations of JSR-330 Standard Annotations -When you work with standard annotations, you should know that some significant -features are not available, as the following table shows: +When you work with JSR-330 standard annotations, you should know that some significant +features are not available, as the following table shows. [[annotations-comparison]] -.Spring component model elements versus JSR-330 variants +.Spring component model versus JSR-330 variants |=== -| Spring| jakarta.inject.*| jakarta.inject restrictions / comments +| Spring | JSR-330 | JSR-330 restrictions / comments -| @Autowired -| @Inject -| `@Inject` has no 'required' attribute. Can be used with Java 8's `Optional` instead. +| `@Autowired` +| `@Inject` +| `@Inject` has no `required` attribute. Can be used with Java's `Optional` instead. -| @Component -| @Named / @ManagedBean +| `@Component` +| `@Named` | JSR-330 does not provide a composable model, only a way to identify named components. -| @Scope("singleton") -| @Singleton +| `@Scope("singleton")` +| `@Singleton` | The JSR-330 default scope is like Spring's `prototype`. However, in order to keep it consistent with Spring's general defaults, a JSR-330 bean declared in the Spring container is a `singleton` by default. In order to use a scope other than `singleton`, you should use Spring's `@Scope` annotation. `jakarta.inject` also provides a - `jakarta.inject.Scope` annotation: however, this one is only intended to be used + `jakarta.inject.Scope` annotation; however, this one is only intended to be used for creating custom annotations. -| @Qualifier -| @Qualifier / @Named +| `@Qualifier` +| `@Qualifier` / `@Named` | `jakarta.inject.Qualifier` is just a meta-annotation for building custom qualifiers. Concrete `String` qualifiers (like Spring's `@Qualifier` with a value) can be associated through `jakarta.inject.Named`. -| @Value +| `@Value` | - | no equivalent -| @Lazy +| `@Lazy` | - | no equivalent -| ObjectFactory -| Provider +| `ObjectFactory` +| `Provider` | `jakarta.inject.Provider` is a direct alternative to Spring's `ObjectFactory`, only with a shorter `get()` method name. It can also be used in combination with Spring's `@Autowired` or with non-annotated constructors and setter methods. |=== - - - diff --git a/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc b/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc index afa9a50dc96a..af18ce82e605 100644 --- a/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc +++ b/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc @@ -3,8 +3,8 @@ Java NIO provides `ByteBuffer` but many libraries build their own byte buffer API on top, especially for network operations where reusing buffers and/or using direct buffers is -beneficial for performance. For example Netty has the `ByteBuf` hierarchy, Undertow uses -XNIO, Jetty uses pooled byte buffers with a callback to be released, and so on. +beneficial for performance. For example Netty has the `ByteBuf` hierarchy, +Jetty uses pooled byte buffers with a callback to be released, and so on. The `spring-core` module provides a set of abstractions to work with various byte buffer APIs as follows: @@ -15,8 +15,6 @@ xref:core/databuffer-codec.adoc#databuffers-buffer-pooled[pooled]. * <> decode or encode data buffer streams into higher level objects. - - [[databuffers-factory]] == `DataBufferFactory` @@ -29,12 +27,10 @@ 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. - - [[databuffers-buffer]] == `DataBuffer` @@ -50,8 +46,6 @@ alternate between read and write. * Determine the index, or the last index, for a given byte. - - [[databuffers-buffer-pooled]] == `PooledDataBuffer` @@ -75,14 +69,12 @@ to use the convenience methods in `DataBufferUtils` that apply release or retain `DataBuffer` only if it is an instance of `PooledDataBuffer`. - - [[databuffers-utils]] == `DataBufferUtils` `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`. @@ -91,8 +83,6 @@ composite buffers, if that's supported by the underlying byte buffer API. * Skip or take from a stream of bytes until a specific byte count. - - [[codecs]] == Codecs @@ -107,8 +97,6 @@ Jackson Smile, JAXB2, Protocol Buffers and other encoders and decoders. See xref:web/webflux/reactive-spring.adoc#webflux-codecs[Codecs] in the WebFlux section. - - [[databuffers-using]] == Using `DataBuffer` @@ -144,7 +132,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 +150,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 7700929f7b60..e7ed47807d3e 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions.adoc @@ -7,14 +7,14 @@ similar to the https://jakarta.ee/specifications/expression-language/[Jakarta Ex 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 {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. +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 within the {spring-site-tools}[Spring Tools] IDE support. That said, SpEL +is based on a technology-agnostic API that lets other expression language implementations +be integrated, should the need arise. While SpEL serves as the foundation for expression evaluation within the Spring portfolio, it is not directly tied to Spring and can be used independently. To @@ -54,4 +54,3 @@ The expression language supports the following functionality: * Collection projection * Collection selection * Templated expressions - diff --git a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc index 85b28932d13a..80a1a85d663e 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc @@ -12,7 +12,7 @@ expression, `Hello World`. ====== 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> @@ -22,7 +22,7 @@ Java:: 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> @@ -51,7 +51,7 @@ literal, `Hello World`. ====== 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> @@ -61,7 +61,7 @@ Java:: 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> @@ -77,7 +77,7 @@ string literal, `Hello World`. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); @@ -89,7 +89,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() @@ -110,7 +110,7 @@ The following example shows how to use dot notation to get the length of a strin ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); @@ -122,7 +122,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() @@ -140,7 +140,7 @@ example shows. ====== 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> @@ -150,7 +150,7 @@ Java:: 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> @@ -173,7 +173,7 @@ reference the `name` property in a boolean expression. ====== 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() @@ -217,35 +217,210 @@ 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. - -* `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. -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. - -* Custom `PropertyAccessor` only (no reflection) -* Data binding properties for read-only access -* Data binding properties for read and write - +`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. + +`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: + +* 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` + +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-evaluation-context-security]] +=== Security Considerations + +SpEL is a powerful expression language that can invoke constructors and methods, read and +write properties and fields, and reference beans – all backed by reflection. Because of +this power, evaluating a SpEL expression obtained from an untrusted source is inherently +dangerous and should generally be avoided, since doing so can effectively grant that +source the ability to execute arbitrary code within the application, regardless of which +`EvaluationContext` implementation is used. + +Throughout this section, a source of a SpEL expression is considered "trusted" only if it +is a developer of the application or an administrator responsible for configuring or +operating the application. Any other source of a SpEL expression must be treated as +untrusted – for example, an expression supplied by an end user of the application or +received from an external system. + +[WARNING] +==== +`StandardEvaluationContext` exposes the complete SpEL language and must *never* be used +to evaluate an expression obtained from an untrusted source. +==== + +Although `SimpleEvaluationContext` restricts the SpEL language to a subset of its +features, that restriction is provided on a best-effort basis and does not guarantee that +expression evaluation is safe. Since an expression can potentially invoke any property, +method, or function reachable via the configured root object, property accessors, method +resolvers, variables, and functions, care must be taken if you choose to evaluate +expressions from an untrusted source. It is therefore the responsibility of the code that +configures an `EvaluationContext` – for example, by supplying a root object or by +registering property accessors, resolvers, variables, or functions – to ensure that none +of the objects reachable via the context expose operations that would be dangerous if +invoked by an expression from an untrusted source. + +Furthermore, a property "getter" reachable from an expression is not necessarily a pure, +side-effect-free read operation. A JavaBean-style accessor (such as `getName()` or +`isActive()`) and a plain accessor method used to support data classes such as Java +records and Kotlin data classes (such as `name()`) are indistinguishable from a method +that performs an action and happens to return a value (that is, a method which is +*accessor-shaped*). For example, the `public boolean delete()` method in `java.io.File` +looks like a plain accessor method to SpEL. Specifically, neither +`ReflectivePropertyAccessor` nor `DataBindingPropertyAccessor` can determine whether such +a method is free of side effects. Moreover, restricting a `SimpleEvaluationContext` to +read-only data binding governs only whether *assignment* to a property is permitted: it +does not verify that reading a property is side-effect-free. When exposing a root object +or other reachable object to an untrusted expression, you must ensure that none of its +accessor-shaped methods perform an action that would be unsafe if triggered by that +expression. + +[NOTE] +.What makes a method "accessor-shaped"? +==== +A method is accessor-shaped if it is `public`, takes no arguments, and returns a value – +the same shape that `ReflectivePropertyAccessor` and `DataBindingPropertyAccessor` look +for when resolving a property "getter" by name. That shape says nothing about whether +invoking the method is actually free of side effects. For example, the following methods +are all accessor-shaped, but only some of them are safe to invoke as a property read. + +Side-effect-free (safe to expose as properties): + +* `getName()` and `isActive()`: conventional JavaBean-style accessors. +* `name()` and `active()`: plain accessor methods used by data classes such as Java + records and Kotlin data classes. + +Side-effecting (unsafe to expose as properties, despite the identical shape): + +* `java.io.File#delete()`: deletes the underlying file and returns whether the deletion + succeeded. +* `java.util.Queue#poll()`: removes and returns the head element, mutating the queue. +* `java.util.concurrent.atomic.AtomicInteger#incrementAndGet()`: increments and returns + a counter, mutating it. + +If an untrusted expression can reference `someFile.delete`, `someQueue.poll`, or +`someCounter.incrementAndGet` as a property, SpEL invokes the corresponding method just +as readily as it would invoke a genuine getter. +==== + +[[expressions-evaluation-context-object-design]] +=== Object Design + +Similar to the design guidance for +xref:web/webmvc/mvc-data-binding.adoc#mvc-data-binding-design[web data binding], you +should carefully design any object that may be reached from a SpEL expression evaluated +against untrusted input. This applies not only to the root object supplied to an +`EvaluationContext` but also to every object that such an expression can navigate to from +that root object – for example, an object returned by a property, a method, an index +operation, a variable, or a function. + +When exposing an object to expressions from an untrusted source, consider the following +recommendations. + +Use a dedicated type:: + Prefer a dedicated type, designed specifically to be evaluated against untrusted + expressions, over passing an existing domain or infrastructure type "as is". A + dedicated type lets you control exactly which properties and methods are reachable from + an expression, rather than exposing the full surface area of a class such as a JPA + entity, `java.io.File`, or a JDBC `Connection` – most of which were never designed with + SpEL evaluation in mind. + +Prefer immutability:: + An immutable type – for example, a Java record or a Kotlin data class exposing only + `val` properties – rules out property writes and eliminates any concern that a "getter" + might mutate state as a side effect, since there is no mutable state to affect. + Immutability does not, on its own, rule out an accessor-shaped method with an external + side effect (such as a network call or a file system operation), but it removes an + entire class of risk. + +Limit scope:: + Expose only the properties and methods that the expression is expected to use, and + nothing more. Because a `PropertyAccessor` cannot restrict access to specific + properties or methods on a per-expression basis, every accessor-shaped method reachable + on an exposed object is reachable by any expression that can reach that object – + regardless of which property or method the application intended the expression to use. + +Audit accessor-shaped methods:: + Review every accessor-shaped method exposed by a type before making it reachable from + an untrusted expression, keeping the <> discussed above in mind. None of the reachable methods should + perform an action that would be unsafe if triggered by that expression. + +[WARNING] +==== +These recommendations apply transitively. If the root object exposes a property or method +that returns another object, and an untrusted expression can navigate to it (for example, +`rootObject.child.grandchild`), the nested object is just as reachable as the root object +itself and must meet the same design requirements. The same is true for an object reached +via indexing (for example, `rootObject.items[0]` or `rootObject.items['key']`): whatever +is returned by the index operation is just as reachable as any other nested object. +==== + +[[expressions-evaluation-context-lifecycle]] +=== Lifecycle and Reuse + +For performance, the AST nodes that make up a parsed `Expression` may cache the specific +`PropertyAccessor`, `IndexAccessor`, `MethodExecutor`, or `ConstructorExecutor` that +satisfied a previous evaluation, so that later evaluations of the same node can avoid +asking every registered accessor or resolver in turn. Understanding this caching behavior +is essential to using `Expression` and `EvaluationContext` correctly, in addition to the +<> discussed previously. + +A parsed `Expression` is designed to be created once and evaluated repeatedly, and doing +so is both supported and encouraged. In particular: + +* A parsed `Expression` may be evaluated against different root objects, and against + different `EvaluationContext` instances of the *same type and with equivalent + configuration* – for example, several `StandardEvaluationContext` instances each + registering the same kind of custom `PropertyAccessor`. Changing the accessors or + resolvers registered with a context between evaluations of the same expression is + atypical and generally not advised, but is expected to work correctly: the registered + state of the *current* context is what is consulted, not a snapshot taken during an + earlier evaluation. +* A parsed `Expression` must *not* be evaluated first against a context with one set of + security implications and later against a context with different, typically more + restrictive, security implications – for example, first against a + `StandardEvaluationContext` and later against a `SimpleEvaluationContext`. Doing so is + analogous to executing a database query on behalf of an administrator, caching the + resulting administrator-privileged execution plan, and then reusing that cached plan for + a lower-privileged user while expecting the lower-privileged user's restrictions to + apply: cached state from the first, more permissive evaluation may be reused during the + second, and the second context's restrictions cannot be reliably enforced as a result. + If the same expression string must be evaluated under contexts with different security + implications, parse it into *distinct* `Expression` instances, one per context. + +[WARNING] +==== +Reusing a single parsed `Expression` across `EvaluationContext` instances with different +security implications is not a supported usage pattern and must be avoided, regardless of +which `EvaluationContext` implementations are involved. +==== [[expressions-type-conversion]] === Type Conversion @@ -266,7 +441,7 @@ being placed in it. 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"] ---- class Simple { public List booleanList = new ArrayList<>(); @@ -287,7 +462,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() @@ -314,23 +489,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; @@ -355,7 +530,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 @@ -389,6 +564,16 @@ set a JVM system property or Spring property named `spring.context.expression.ma to the maximum expression length needed by your application (see xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]). +Similarly, the number of operations performed during the evaluation of a SpEL expression +cannot exceed 10,000 by default; however, the `maxOperations` value is configurable. If +you create a `SpelExpressionParser` programmatically (the recommend approach), you can +specify a custom `maxOperations` value when creating the `SpelParserConfiguration` that +you provide to the `SpelExpressionParser`. If you are not able to configure an explicit +value for `maxOperations` via `SpelParserConfiguration`, you can set a JVM system +property or Spring property named `spring.expression.maxOperations` to the maximum number +of operations required by your application (see +xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]). + [[expressions-spel-compilation]] == SpEL Compilation @@ -422,7 +607,6 @@ numeric operations, the performance gain can be very noticeable. In an example m 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]] === Compiler Configuration @@ -435,18 +619,27 @@ 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. +`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 @@ -461,7 +654,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"] ---- SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, this.getClass().getClassLoader()); @@ -477,7 +670,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) @@ -506,7 +699,6 @@ 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`). - [[expressions-compiler-limitations]] === Compiler Limitations @@ -520,6 +712,6 @@ following kinds of expressions cannot be compiled. * Expressions using overloaded operators * Expressions using array construction syntax * Expressions using selection or projection +* Expressions using bean references 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 b85702e3ae7b..2e63732cee1f 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/example-classes.adoc @@ -9,7 +9,7 @@ This section lists the classes used in the examples throughout this chapter. ====== 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; @@ -84,7 +84,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.spring.samples.spel.inventor @@ -103,7 +103,7 @@ Kotlin:: ====== 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; @@ -141,7 +141,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.spring.samples.spel.inventor @@ -155,7 +155,7 @@ Kotlin:: ====== 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; @@ -200,7 +200,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.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 ebce4c294cc4..5cee5eac42bf 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref.adoc @@ -2,4 +2,12 @@ = Language Reference :page-section-summary-toc: 1 -This section describes how the Spring Expression Language works. +Spring Expression Language (SpEL) expressions are composed of a sequence of tokens such +as literals, operators, method invocations, and so forth. + +Whitespace can be used freely between tokens to format and improve the readability of +expressions. Specifically, the `\s` (space), `\t` (tab), `\r` (carriage return), and `\n` +(newline) characters are all valid separators between tokens. However, whitespace is +ignored by the expression parser unless it is part of a string literal. + +The following sections describe the features and syntax of SpEL. 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 4bc931742295..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,7 +8,7 @@ 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); @@ -21,7 +21,7 @@ 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 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 78a3ec21fe4f..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 @@ -11,7 +11,7 @@ list. The following example uses projection to do so: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // evaluates to ["Smiljan", "Idvor"] List placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") @@ -20,7 +20,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // evaluates to ["Smiljan", "Idvor"] val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") 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 b87bc1733413..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 @@ -41,14 +41,14 @@ than 27: ====== 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.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() as Map ---- 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 6e1e2bf81f64..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 @@ -2,8 +2,12 @@ = Functions You can extend SpEL by registering user-defined functions that can be called within -expressions by using the `#functionName(...)` syntax. Functions can be registered as -variables in `EvaluationContext` implementations via the `setVariable()` method. +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] ==== @@ -26,7 +30,7 @@ reflection using a `java.lang.reflect.Method`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Method method = ...; @@ -36,7 +40,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val method: Method = ... @@ -51,7 +55,7 @@ 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 { @@ -63,7 +67,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun reverseString(input: String): String { return StringBuilder(input).reverse().toString() @@ -77,7 +81,7 @@ You can register and use the preceding method, as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); @@ -92,7 +96,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() @@ -110,8 +114,9 @@ potentially more efficient use cases if the `MethodHandle` target and parameters been fully bound prior to registration; however, partially bound handles are also supported. -Consider the `String#formatted(String, Object...)` instance method, which produces a -message according to a template and a variable number of arguments. +Consider the `String#formatted(Object...)` instance method, which produces a message +according to a template and a variable number of arguments +(xref:core/expressions/language-ref/varargs.adoc[varargs]). You can register and use the `formatted` method as a `MethodHandle`, as the following example shows: @@ -120,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(); @@ -136,7 +141,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() @@ -151,16 +156,16 @@ Kotlin:: ---- ====== -As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also -supported. This is likely to be more performant if both the target and all the arguments -are bound. In that case no arguments are necessary in the SpEL expression, as the -following example shows: +As mentioned above, binding a `MethodHandle` and registering the bound `MethodHandle` is +also supported. This is likely to be more performant if both the target and all the +arguments are bound. In that case no arguments are necessary in the SpEL expression, as +the following example shows: [tabs] ====== 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(); @@ -168,9 +173,10 @@ Java:: String template = "This is a %s message with %s words: <%s>"; Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" }; MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted", - MethodType.methodType(String.class, Object[].class)) + MethodType.methodType(String.class, Object[].class)) .bindTo(template) - .bindTo(varargs); //here we have to provide arguments in a single array binding + // Here we have to provide the arguments in a single array binding: + .bindTo(varargs); context.setVariable("message", mh); // evaluates to "This is a prerecorded message with 3 words: " @@ -180,7 +186,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() @@ -189,9 +195,10 @@ Kotlin:: val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored") val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted", - MethodType.methodType(String::class.java, Array::class.java)) + MethodType.methodType(String::class.java, Array::class.java)) .bindTo(template) - .bindTo(varargs) //here we have to provide arguments in a single array binding + // Here we have to provide the arguments in a single array binding: + .bindTo(varargs) context.setVariable("message", mh) // evaluates to "This is a prerecorded message with 3 words: " @@ -201,4 +208,3 @@ Kotlin:: ====== - 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 52b1c9c06ced..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 @@ -53,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(); @@ -75,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 3de85d474d4b..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 @@ -27,7 +27,7 @@ The following example shows how to use the safe navigation operator for property ====== 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(); @@ -50,7 +50,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() @@ -100,7 +100,7 @@ a list (`?.[]`). ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); IEEE society = new IEEE(); @@ -121,7 +121,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val society = IEEE() @@ -161,7 +161,7 @@ selection (`?.?`). ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); IEEE society = new IEEE(); @@ -182,7 +182,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val society = IEEE() @@ -209,7 +209,7 @@ collections (`?.^`). ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); IEEE society = new IEEE(); @@ -231,7 +231,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val society = IEEE() @@ -252,7 +252,6 @@ Kotlin:: <1> Use "null-safe select first" operator on potentially null `members` list ====== - The following example shows how to use the "null-safe select last" operator for collections (`?.$`). @@ -260,7 +259,7 @@ collections (`?.$`). ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); IEEE society = new IEEE(); @@ -282,7 +281,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val society = IEEE() @@ -310,7 +309,7 @@ projection (`?.!`). ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); IEEE society = new IEEE(); @@ -331,7 +330,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val society = IEEE() @@ -351,6 +350,50 @@ Kotlin:: <2> Use null-safe projection operator on null `members` list ====== +[[expressions-operator-safe-navigation-optional]] +== Null-safe Operations on `Optional` + +As of Spring Framework 7.0, null-safe operations are supported on instances of +`java.util.Optional` with transparent unwrapping semantics. + +Specifically, when a null-safe operator is applied to an _empty_ `Optional`, it will be +treated as if the `Optional` were `null`, and the subsequent operation will evaluate to +`null`. However, if a null-safe operator is applied to a non-empty `Optional`, the +subsequent operation will be applied to the object contained in the `Optional`, thereby +effectively unwrapping the `Optional`. + +For example, if `user` is of type `Optional`, the expression `user?.name` will +evaluate to `null` if `user` is either `null` or an _empty_ `Optional` and will otherwise +evaluate to the `name` of the `user`, effectively `user.get().getName()` or +`user.get().name` for property or field access, respectively. + +[NOTE] +==== +Invocations of methods defined in the `Optional` API are still supported on an _empty_ +`Optional`. For example, if `name` is of type `Optional`, the expression +`name?.orElse('Unknown')` will evaluate to `"Unknown"` if `name` is an empty `Optional` +and will otherwise evaluate to the `String` contained in the `Optional` if `name` is a +non-empty `Optional`, effectively `name.get()`. +==== + +// NOTE: ⁠ is the Unicode Character 'WORD JOINER', which prevents undesired line wraps. + +Similarly, if `names` is of type `Optional>`, the expression +`names?.?⁠[#this.length > 5]` will evaluate to `null` if `names` is `null` or an _empty_ +`Optional` and will otherwise evaluate to a sequence containing the names whose lengths +are greater than 5, effectively +`names.get().stream().filter(s -> s.length() > 5).toList()`. + +The same semantics apply to all of the null-safe operators mentioned previously in this +chapter. + +For further details and examples, consult the javadoc for the following operators. + +* {spring-framework-api}/expression/spel/ast/PropertyOrFieldReference.html[`PropertyOrFieldReference`] +* {spring-framework-api}/expression/spel/ast/MethodReference.html[`MethodReference`] +* {spring-framework-api}/expression/spel/ast/Indexer.html[`Indexer`] +* {spring-framework-api}/expression/spel/ast/Selection.html[`Selection`] +* {spring-framework-api}/expression/spel/ast/Projection.html[`Projection`] [[expressions-operator-safe-navigation-compound-expressions]] == Null-safe Operations in Compound Expressions @@ -380,7 +423,7 @@ evaluates to `null` instead of throwing an exception. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ExpressionParser parser = new SpelExpressionParser(); IEEE society = new IEEE(); @@ -401,7 +444,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val parser = SpelExpressionParser() val society = IEEE() 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 f658d77d4410..37de2a3aedbf 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 @@ -24,7 +24,7 @@ The following listing shows a few examples of relational operators: ====== 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); @@ -41,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) @@ -53,7 +53,7 @@ Kotlin:: val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java) // uses CustomValue:::compareTo - val trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean::class.java); + val trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean::class.java) ---- ====== @@ -89,7 +89,7 @@ shows examples of all three: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- boolean result; @@ -128,7 +128,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // evaluates to true var result = parser.parseExpression( @@ -167,7 +167,7 @@ Kotlin:: [CAUTION] ==== The syntax for the `between` operator is ` between {, }`, -which is effectively a shortcut for ` >= && \<= }`. +which is effectively a shortcut for ` >= && \<= `. Consequently, `1 between {1, 5}` evaluates to `true`, while `1 between {5, 1}` evaluates to `false`. @@ -195,7 +195,7 @@ The following example shows how to use the logical operators: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // -- AND -- @@ -228,7 +228,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // -- AND -- @@ -277,7 +277,7 @@ The following example shows the `String` operators in use: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // -- Concatenation -- @@ -300,7 +300,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // -- Concatenation -- @@ -312,13 +312,13 @@ Kotlin:: // evaluates to 'a' val ch = parser.parseExpression("'d' - 3") - .getValue(Character::class.java); + .getValue(Char::class.java) // -- Repeat -- // evaluates to "abcabc" val repeated = parser.parseExpression("'abc' * 2") - .getValue(String::class.java); + .getValue(String::class.java) ---- ====== @@ -358,7 +358,7 @@ The following example shows the mathematical operators in use: ====== 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(); @@ -424,7 +424,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() @@ -485,7 +485,7 @@ Kotlin:: // -- Operator precedence -- - val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21 + val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21 ---- ====== @@ -501,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(); @@ -515,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() @@ -541,32 +541,7 @@ 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",role="primary"] ----- - 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)); - } - } ----- +include-code::./ListConcatenation[] If we register `ListConcatenation` as the `OperatorOverloader` in a `StandardEvaluationContext`, we can then evaluate expressions like `{1, 2, 3} + {4, 5}` @@ -576,7 +551,7 @@ as demonstrated in the following example. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- StandardEvaluationContext context = new StandardEvaluationContext(); context.setOperatorOverloader(new ListConcatenation()); @@ -587,10 +562,10 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - StandardEvaluationContext context = StandardEvaluationContext() - context.setOperatorOverloader(ListConcatenation()) + val context = StandardEvaluationContext() + context.operatorOverloader = ListConcatenation() // evaluates to a new list: [1, 2, 3, 4, 5] parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List::class.java) 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 f599037490c7..a55e91a8acb3 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 @@ -25,7 +25,7 @@ we use the following expressions: ====== 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); @@ -36,7 +36,7 @@ Java:: 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 @@ -74,7 +74,7 @@ the collection using its `Iterator` and returning the n^th^ element encountered. ====== 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(); @@ -100,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() @@ -138,7 +138,7 @@ NOTE: The n^th^ character of a string will evaluate to a `java.lang.String`, not ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // evaluates to "T" (8th letter of "Nikola Tesla") String character = parser.parseExpression("members[0].name[7]") @@ -147,7 +147,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // evaluates to "T" (8th letter of "Nikola Tesla") val character = parser.parseExpression("members[0].name[7]") @@ -166,7 +166,7 @@ string literals such as `'president'`: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Officer's Map @@ -191,7 +191,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Officer's Map @@ -227,7 +227,7 @@ property. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Create an inventor to use as the root context object. Inventor tesla = new Inventor("Nikola Tesla"); @@ -239,7 +239,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Create an inventor to use as the root context object. val tesla = Inventor("Nikola Tesla") @@ -270,7 +270,7 @@ 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. +invocable 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 @@ -325,7 +325,7 @@ into a `FruitMap` and then index into the `FruitMap` within a SpEL expression. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Create a ReflectiveIndexAccessor for FruitMap IndexAccessor fruitMapAccessor = new ReflectiveIndexAccessor( @@ -344,7 +344,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Create a ReflectiveIndexAccessor for FruitMap val fruitMapAccessor = ReflectiveIndexAccessor( 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 1603fe0db298..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 @@ -10,7 +10,7 @@ shows: ====== 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()}", 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 ff285e28b584..9faaee046245 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 @@ -6,7 +6,7 @@ are set by using the `setVariable()` method in `EvaluationContext` implementatio [NOTE] ==== -Variable names must be begin with a letter (as defined below), an underscore, or a dollar +Variable names must 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 @@ -42,7 +42,7 @@ The following example shows how to use variables. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); @@ -55,7 +55,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val tesla = Inventor("Nikola Tesla", "Serbian") @@ -83,7 +83,7 @@ xref:core/expressions/language-ref/collection-selection.adoc[collection selectio ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Create a list of prime integers. List primes = List.of(2, 3, 5, 7, 11, 13, 17); @@ -103,7 +103,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Create a list of prime integers. val primes = listOf(2, 3, 5, 7, 11, 13, 17) @@ -130,7 +130,7 @@ xref:core/expressions/language-ref/collection-projection.adoc[collection project ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Create parser and evaluation context. ExpressionParser parser = new SpelExpressionParser(); @@ -154,7 +154,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Create parser and evaluation context. val parser = SpelExpressionParser() diff --git a/framework-docs/modules/ROOT/pages/core/null-safety.adoc b/framework-docs/modules/ROOT/pages/core/null-safety.adoc index 8e2fe8ed42be..2460bbe292f4 100644 --- a/framework-docs/modules/ROOT/pages/core/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/core/null-safety.adoc @@ -1,57 +1,216 @@ [[null-safety]] = Null-safety -Although Java does not let you express null-safety with its type system, the Spring Framework -provides the following annotations in the `org.springframework.lang` package to let you -declare nullability of APIs and fields: +Although Java does not let you express nullness markers with its type system yet, the Spring Framework codebase is +annotated with https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullability of its APIs, +fields, and related type usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly +recommended in order to get familiar with those annotations and semantics. -* {spring-framework-api}/lang/Nullable.html[`@Nullable`]: Annotation to indicate that a -specific parameter, return value, or field can be `null`. -* {spring-framework-api}/lang/NonNull.html[`@NonNull`]: Annotation to indicate that a specific -parameter, return value, or field cannot be `null` (not needed on parameters, return values, -and fields where `@NonNullApi` and `@NonNullFields` apply, respectively). -* {spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`]: Annotation at the package level -that declares non-null as the default semantics for parameters and return values. -* {spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`]: Annotation at the package -level that declares non-null as the default semantics for fields. +The primary goal of this null-safety arrangement is to prevent a `NullPointerException` from being thrown at +runtime via build time checks and to use explicit nullability as a way to express the possible absence of value. +It is useful in Java by leveraging nullability checkers such as https://github.com/uber/NullAway[NullAway] or IDEs +supporting JSpecify annotations such as IntelliJ IDEA and Eclipse (the latter requiring manual configuration). In Kotlin, +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, and 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 nullability declarations with https://github.com/uber/NullAway[NullAway] +as part of its build. It is recommended for each library depending on Spring Framework and Spring portfolio projects, +as well as other libraries related to the Spring ecosystem (Reactor, Micrometer, and Spring community projects), +to do the same. -[[use-cases]] -== Use cases +[[null-safety-applications]] +== Leveraging JSpecify annotations in Spring applications -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. +Developing applications with IDEs that support nullness annotations will provide warnings in Java and errors in +Kotlin when the nullability contracts are not honored, allowing Spring application developers to refine their +null handling to prevent a `NullPointerException` from being thrown at runtime. -They are also used to make Spring APIs null-safe in Kotlin projects, since Kotlin natively -supports {kotlin-docs}/null-safety.html[null-safety]. More details -are available in the xref:languages/kotlin/null-safety.adoc[Kotlin support documentation]. +Optionally, Spring application developers can annotate their codebase and use build plugins like +https://github.com/uber/NullAway[NullAway] to enforce null-safety at the application level during build time. +[[null-safety-guidelines]] +== Guidelines +The purpose of this section is to share some proposed guidelines for explicitly specifying the nullability of +Spring-related libraries or applications. +[[null-safety-guidelines-jspecify]] +=== JSpecify -[[jsr-305-meta-annotations]] -== JSR-305 meta-annotations +==== Defaults to non-null -Spring annotations are meta-annotated with {JSR}305[JSR 305] -annotations (a dormant but widespread JSR). JSR-305 meta-annotations let tooling vendors -like IDEA or Kotlin provide null-safety support in a generic way, without having to -hard-code support for Spring annotations. +A key point to understand is that the nullness of types is unknown by default in Java and that non-null type usage +is by far more frequent than nullable usage. In order to keep codebases readable, we typically want to define by +default that type usage is non-null unless marked as nullable for a specific scope. This is exactly the purpose +of https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`] which is typically set +in Spring projects at the package level via a `package-info.java` file, for example: -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. +[source,java,subs="verbatim,quotes",chomp="-packages",fold="none"] +---- +@NullMarked +package org.springframework.core; + +import org.jspecify.annotations.NullMarked; +---- + +==== Explicit nullability + +In `@NullMarked` code, nullable type usage is defined explicitly with +https://jspecify.dev/docs/api/org/jspecify/annotations/Nullable.html[`@Nullable`]. + +A key difference between JSpecify `@Nullable` / `@NonNull` annotations and most other variants is that the JSpecify +annotations are meta-annotated with `@Target(ElementType.TYPE_USE)`, so they apply only to type usage. This impacts +where such annotations should be placed, either to comply with +https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.7.4[related Java specifications] or to follow code +style best practices. From a style perspective, it is recommended to embrace the type-use nature of those annotations +by placing them on the same line as and immediately preceding the annotated type. + +For example, for a field: + +[source,java,subs="verbatim,quotes"] +---- +private @Nullable String fileEncoding; +---- + +Or for method parameters and method return types: + +[source,java,subs="verbatim,quotes"] +---- +public @Nullable String buildMessage(@Nullable String message, + @Nullable Throwable cause) { + // ... +} +---- + +[NOTE] +==== +When overriding a method, JSpecify annotations are not inherited from the original +method. That means the JSpecify annotations should be copied to the overriding method if +you want to override the implementation and keep the same nullability semantics. +==== + +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. + +==== Arrays and varargs + +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. For example, in `@NullMarked` code: + +- `@Nullable Object[] array` means individual elements can be `null` but the array itself cannot. +- `Object @Nullable [] array` means individual elements cannot be `null` but the array itself can. +- `@Nullable Object @Nullable [] array` means both individual elements and the array can be `null`. + +==== Generics + +JSpecify annotations apply to generics as well. For example, in `@NullMarked` code: + + - `List` means a list of non-null elements (equivalent of `List<@NonNull String>`) + - `List<@Nullable String>` means a list of nullable elements + +Things are a bit more complicated when you are declaring generic types or generic methods. See the related +https://jspecify.dev/docs/user-guide/#generics[JSpecify generics documentation] for more details. + +WARNING: The nullability of generic types and generic methods +https://github.com/uber/NullAway/issues?q=is%3Aissue+is%3Aopen+label%3Ajspecify[is not yet fully supported by NullAway]. + +==== Nested and fully qualified types + +The Java specification also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` – like JSpecify's +`@Nullable` annotation – must be declared after the last dot (`.`) within inner or fully qualified type names: + +- `Cache.@Nullable ValueWrapper` +- `jakarta.validation.@Nullable Validator` + + +[[null-safety-guidelines-nullaway]] +=== NullAway + +==== Configuration + +The recommended configuration is: + + - `NullAway:OnlyNullMarked=true` in order to perform nullability checks only for packages annotated with `@NullMarked`. + - `NullAway:CustomContractAnnotations=org.springframework.lang.Contract` which makes NullAway aware of the +{spring-framework-api}/lang/Contract.html[@Contract] annotation in the `org.springframework.lang` package which +can be used to express complementary semantics to avoid irrelevant warnings in your codebase. + +A good example of the benefits of a `@Contract` declaration can be seen with +{spring-framework-api}/util/Assert.html#notNull(java.lang.Object,java.lang.String)[`Assert.notNull()`] +which is annotated with `@Contract("null, _ -> fail")`. With that contract declaration, NullAway will understand +that the value passed as a parameter cannot be null after a successful invocation of `Assert.notNull()`. + +Optionally, it is possible to set `NullAway:JSpecifyMode=true` to enable +https://github.com/uber/NullAway/wiki/JSpecify-Support[checks on the full JSpecify semantics], including annotations on +arrays, varargs, and generics. Be aware that this mode is +https://github.com/uber/NullAway/issues?q=is%3Aissue+is%3Aopen+label%3Ajspecify[still under development] and requires +JDK 22 or later (typically combined with the `--release` Java compiler flag to configure the +expected baseline). It is recommended to enable the JSpecify mode only as a second step, after making sure the codebase +generates no warning with the recommended configuration mentioned previously in this section. + +==== Warnings suppression + +There are a few valid use cases where NullAway will incorrectly detect nullability problems. In such cases, +it is recommended to suppress related warnings and to document the reason: + + - `@SuppressWarnings("NullAway.Init")` at field, constructor, or class level can be used to avoid unnecessary warnings +due to the lazy initialization of fields – for example, due to a class implementing +{spring-framework-api}/beans/factory/InitializingBean.html[`InitializingBean`]. + - `@SuppressWarnings("NullAway") // Dataflow analysis limitation` can be used when NullAway dataflow analysis is not +able to detect that the path involving a nullability problem will never happen. + - `@SuppressWarnings("NullAway") // Lambda` can be used when NullAway does not take into account assertions performed +outside of a lambda for the code path within the lambda. +- `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known to return +non-null values even if that cannot be expressed by the API. +- `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are performed with keys +that are known to be present and when non-null related values have been inserted previously. +- `@SuppressWarnings("NullAway") // Overridden method does not define nullability` can be used when the superclass does +not define nullability (typically when the superclass comes from an external dependency). +- `@SuppressWarnings("NullAway") // See https://github.com/uber/NullAway/issues/1075` can be used when NullAway is not able to detect type variable nullness in generic methods. + + +[[null-safety-migrating]] +== Migrating from Spring null-safety annotations + +Spring null-safety annotations {spring-framework-api}/lang/Nullable.html[`@Nullable`], +{spring-framework-api}/lang/NonNull.html[`@NonNull`], +{spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`], and +{spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`] in the `org.springframework.lang` package were +introduced in Spring Framework 5 when JSpecify did not exist, and the best option at that time was to leverage +meta-annotations from JSR 305 (a dormant but widespread JSR). They are deprecated as of Spring Framework 7 in favor of +https://jspecify.dev/docs/start-here/[JSpecify] annotations, which provide significant enhancements such as properly +defined specifications, a canonical dependency with no split-package issues, better tooling, better Kotlin integration, +and the capability to specify nullability more precisely for more use cases. + +A key difference is that Spring's deprecated null-safety annotations, which follow JSR 305 semantics, apply to fields, +parameters, and return values; while JSpecify annotations apply to type usage. This subtle difference +is pretty significant in practice, since it allows developers to differentiate between the nullness of elements and the +nullness of arrays/varargs as well as to define the nullness of generic types. + +That means array and varargs null-safety declarations have to be updated to keep the same semantics. For example +`@Nullable Object[] array` with Spring annotations needs to be changed to `Object @Nullable [] array` with JSpecify +annotations. The same applies to varargs. + +It is also recommended to move field and return value annotations closer to the type and on the same line, for example: + + - For fields, instead of `@Nullable private String field` with Spring annotations, use `private @Nullable String field` +with JSpecify annotations. +- For method return types, instead of `@Nullable public String method()` with Spring annotations, use +`public @Nullable String method()` with JSpecify annotations. + +Also, with JSpecify, you do not need to specify `@NonNull` when overriding a type usage annotated with `@Nullable` +in the super method to "undo" the nullable declaration in null-marked code. Just declare it unannotated, and the +null-marked defaults will apply (type usage is considered non-null unless explicitly annotated as nullable). diff --git a/framework-docs/modules/ROOT/pages/core/resilience.adoc b/framework-docs/modules/ROOT/pages/core/resilience.adoc new file mode 100644 index 000000000000..fa68ac4fd432 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/core/resilience.adoc @@ -0,0 +1,373 @@ +[[resilience]] += Resilience Features + +As of 7.0, the core Spring Framework includes common resilience features, in particular +<> and <> +annotations for method invocations as well as <>. + + +[[resilience-annotations-retryable]] +== `@Retryable` + +{spring-framework-api}/resilience/annotation/Retryable.html[`@Retryable`] is an annotation +that specifies retry characteristics for an individual method (with the annotation +declared at the method level), or for all proxy-invoked methods in a given class hierarchy +(with the annotation declared at the type level). + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Retryable +public void sendNotification() { + this.jmsClient.destination("notifications").send(...); +} +---- + +By default, the method invocation will be retried for any exception thrown: with at most +3 retry attempts (`maxRetries = 3`) after an initial failure, and a delay of 1 second +between attempts. If all attempts have failed and the retry policy has been exhausted, +the last original exception from the target method will be propagated to the caller. + +[NOTE] +==== +A `@Retryable` method will be invoked at least once and retried at most `maxRetries` +times, where `maxRetries` is the maximum number of retry attempts. Specifically, +`total attempts = 1 initial attempt + maxRetries attempts`. + +For example, if `maxRetries` is set to `4`, the `@Retryable` method will be invoked at +least once and at most 5 times. +==== + +This can be specifically adapted for every method if necessary — for example, by narrowing +the exceptions to retry via the `includes` and `excludes` attributes. The supplied +exception types will be matched against an exception thrown by a failed invocation as well +as nested causes. + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Retryable(MessageDeliveryException.class) +public void sendNotification() { + this.jmsClient.destination("notifications").send(...); +} +---- + +NOTE: `@Retryable(MessageDeliveryException.class)` is a shortcut for +`@Retryable(includes{nbsp}={nbsp}MessageDeliveryException.class)`. + +[TIP] +==== +For advanced use cases, you can specify a custom `MethodRetryPredicate` via the +`predicate` attribute in `@Retryable`, and the predicate will be used to determine whether +to retry a failed method invocation based on a `Method` and a given `Throwable` – for +example, by checking the message of the `Throwable`. + +Custom predicates can be combined with `includes` and `excludes`; however, custom +predicates will always be applied after `includes` and `excludes` have been applied. +==== + +Or for 4 retry attempts and an exponential back-off strategy with a bit of jitter: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Retryable( + includes = MessageDeliveryException.class, + maxRetries = 4, + delay = 100, + jitter = 10, + multiplier = 2, + maxDelay = 1000) +public void sendNotification() { + this.jmsClient.destination("notifications").send(...); +} +---- + +[NOTE] +==== +When `delay` is `0` combined with a positive `jitter`, the delay never grows +regardless of any configured `multiplier`, so the full configured `jitter` is +applied directly as a random delay in the range from `0` to `min(jitter, maxDelay)`. +==== + +Last but not least, `@Retryable` also works for reactive methods with a reactive return +type, decorating the pipeline with Reactor's retry capabilities: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Retryable(maxRetries = 4, delay = 100) +public Mono sendNotification() { + return Mono.from(...); // <1> +} +---- +<1> This raw `Mono` will get decorated with a retry spec. + +For details on the various characteristics, see the available annotation attributes in +{spring-framework-api}/resilience/annotation/Retryable.html[`@Retryable`]. + +TIP: Several attributes in `@Retryable` have `String` variants that provide property +placeholder and SpEL support, as an alternative to the specifically typed annotation +attributes used in the above examples. + +[TIP] +==== +During `@Retryable` processing, Spring publishes a `MethodRetryEvent` for every exception +coming out of the target method. This can be used to track/log all original exceptions +whereas the caller of the `@Retryable` method will only ever see the last exception. +==== + + +[[resilience-annotations-concurrencylimit]] +== `@ConcurrencyLimit` + +{spring-framework-api}/resilience/annotation/ConcurrencyLimit.html[`@ConcurrencyLimit`] is +an annotation that specifies a concurrency limit for an individual method (with the +annotation declared at the method level), or for all proxy-invoked methods in a given +class hierarchy (with the annotation declared at the type level). + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@ConcurrencyLimit(10) +public void sendNotification() { + this.jmsClient.destination("notifications").send(...); +} +---- + +This is meant to protect the target resource from being accessed from too many threads at +the same time, similar to the effect of a pool size limit for a thread pool or a +connection pool that blocks access if its limit is reached. + +You may optionally set the limit to `1`, effectively locking access to the target bean +instance: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@ConcurrencyLimit(1) +public void sendNotification() { + this.jmsClient.destination("notifications").send(...); +} +---- + +Such limiting is particularly useful with Virtual Threads where there is generally no +thread pool limit in place. For asynchronous tasks, this can be constrained on +{spring-framework-api}/core/task/SimpleAsyncTaskExecutor.html[`SimpleAsyncTaskExecutor`]. +For synchronous invocations, this annotation provides equivalent behavior through +{spring-framework-api}/aop/interceptor/ConcurrencyThrottleInterceptor.html[`ConcurrencyThrottleInterceptor`] +which has been available since Spring Framework 1.0 for programmatic use with the AOP +framework. + +TIP: `@ConcurrencyLimit` also has a `limitString` attribute that provides property +placeholder and SpEL support, as an alternative to the `int` based examples above. + + +[[resilience-annotations-configuration]] +== Enabling Resilient Methods + +Like many of Spring's core annotation-based features, `@Retryable` and `@ConcurrencyLimit` +are designed as metadata that you can choose to honor or ignore. The most convenient way +to enable processing of the resilience annotations is to declare +{spring-framework-api}/resilience/annotation/EnableResilientMethods.html[`@EnableResilientMethods`] +on a corresponding `@Configuration` class. + +Alternatively, these annotations can be individually enabled by defining a +`RetryAnnotationBeanPostProcessor` or a `ConcurrencyLimitBeanPostProcessor` bean in the +context. + + +[[resilience-programmatic-retry]] +== Programmatic Retry Support + +In contrast to <> which provides a declarative approach +for specifying retry semantics for methods within beans registered in the +`ApplicationContext`, +{spring-framework-api}/core/retry/RetryTemplate.html[`RetryTemplate`] provides a +programmatic API for retrying arbitrary blocks of code. + +Specifically, a `RetryTemplate` executes and potentially retries a +{spring-framework-api}/core/retry/Retryable.html[`Retryable`] operation based on a +configured {spring-framework-api}/core/retry/RetryPolicy.html[`RetryPolicy`]. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + var retryTemplate = new RetryTemplate(); // <1> + + retryTemplate.invoke( + () -> jmsClient.destination("notifications").send(...)); +---- +<1> Implicitly uses `RetryPolicy.withDefaults()`. + +By default, a retryable operation will be retried for any exception thrown: with at most +3 retry attempts (`maxRetries = 3`) after an initial failure, and a delay of 1 second +between attempts. + +[NOTE] +==== +A retryable operation will be executed at least once and retried at most `maxRetries` +times, where `maxRetries` is the maximum number of retry attempts. Specifically, +`total attempts = 1 initial attempt + maxRetries attempts`. + +For example, if `maxRetries` is set to `4`, the retryable operation will be invoked at +least once and at most 5 times. +==== + +If you only need to customize the number of retry attempts, you can use the +`RetryPolicy.withMaxRetries()` factory method as demonstrated below. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + var retryTemplate = new RetryTemplate(RetryPolicy.withMaxRetries(4)); // <1> + + retryTemplate.invoke( + () -> jmsClient.destination("notifications").send(...)); +---- +<1> Explicitly uses `RetryPolicy.withMaxRetries(4)`. + +If you need to narrow the types of exceptions to retry, that can be achieved via the +`includes()` and `excludes()` builder methods. The supplied exception types will be +matched against an exception thrown by a failed operation as well as nested causes. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + var retryPolicy = RetryPolicy.builder() + .includes(MessageDeliveryException.class) // <1> + .excludes(...) // <2> + .build(); + + var retryTemplate = new RetryTemplate(retryPolicy); + + retryTemplate.invoke( + () -> jmsClient.destination("notifications").send(...)); +---- +<1> Specify one or more exception types to include. +<2> Specify one or more exception types to exclude. + +[TIP] +==== +For advanced use cases, you can specify a custom `Predicate` via the +`predicate()` method in the `RetryPolicy.Builder`, and the predicate will be used to +determine whether to retry a failed operation based on a given `Throwable` – for example, +by checking the message of the `Throwable`. + +Custom predicates can be combined with `includes` and `excludes`; however, custom +predicates will always be applied after `includes` and `excludes` have been applied. +==== + +The following example demonstrates how to configure a `RetryPolicy` with 4 retry attempts +and an exponential back-off strategy with a bit of jitter. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + var retryPolicy = RetryPolicy.builder() + .includes(MessageDeliveryException.class) + .maxRetries(4) + .delay(Duration.ofMillis(100)) + .jitter(Duration.ofMillis(10)) + .multiplier(2) + .maxDelay(Duration.ofSeconds(1)) + .build(); + + var retryTemplate = new RetryTemplate(retryPolicy); + + retryTemplate.invoke( + () -> jmsClient.destination("notifications").send(...)); +---- + +[NOTE] +==== +When `delay` is zero combined with a positive `jitter`, the delay never grows +regardless of any configured `multiplier`, so the full configured `jitter` is +applied directly as a random delay in the range from zero to `min(jitter, maxDelay)`. +==== + +[TIP] +==== +Although the factory methods and builder API for `RetryPolicy` cover most common +configuration scenarios, you can implement a custom `RetryPolicy` for complete control +over the types of exceptions that should trigger a retry as well as the +{spring-framework-api}/util/backoff/BackOff.html[`BackOff`] strategy to use. Note that +you can also configure a customized `BackOff` strategy via the `backOff()` method in +the `RetryPolicy.Builder`. +==== + +Note that the examples above apply a pattern similar to `@Retryable` method invocations +where the last original exception will be propagated to the caller, using the `invoke` +variants on `RetryTemplate` which are available with and without a return value. +The callback may throw unchecked exceptions, the last one of which is exposed for +direct handling on the caller side: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + try { + retryTemplate.invoke( + () -> jmsClient.destination("notifications").send(...)); + } + catch (MessageDeliveryException ex) { + // coming out of the original JmsClient send method + } +---- + +[source,java,indent=0,subs="verbatim,quotes"] +---- + try { + var result = retryTemplate.invoke(() -> { + jmsClient.destination("notifications").send(...); + return "result"; + }); + } + catch (MessageDeliveryException ex) { + // coming out of the original JmsClient send method + } +---- + +`RetryTemplate` instances are very light and can be created on the fly, +potentially with a specific retry policy to use for a given invocation: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + try { + new RetryTemplate(RetryPolicy.withMaxRetries(4)).invoke( + () -> jmsClient.destination("notifications").send(...)); + } + catch (MessageDeliveryException ex) { + // coming out of the original JmsClient send method + } +---- + +For deeper interaction, you may use RetryTemplate's `execute` method. The caller will +have to handle the checked `RetryException` thrown by `RetryTemplate`, exposing the +outcome of all attempts: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + try { + var result = retryTemplate.execute(() -> { + jmsClient.destination("notifications").send(...); + return "result"; + }); + } + catch (RetryException ex) { + // ex.getExceptions() / ex.getLastException() ... + } +---- + +A {spring-framework-api}/core/retry/RetryListener.html[`RetryListener`] can be registered +with a `RetryTemplate` to react to key retry steps (before or after a retry attempt etc.) +or simply to every invocation attempt, being able to track all exceptions coming out of +the callback and all retry outcomes (exhaustion, interruption, timeout). This is +particularly useful when using `invoke` where no retry state other than the last +original exception is exposed otherwise: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + var retryTemplate = new RetryTemplate(); + retryTemplate.setRetryListener(new RetryListener() { + @Override + public void onRetryableExecution(RetryPolicy retryPolicy, Retryable retryable, RetryState retryState) { + ... + } + }); + + retryTemplate.invoke( + () -> jmsClient.destination("notifications").send(...)); +---- + +You can also compose multiple listeners via a +{spring-framework-api}/core/retry/support/CompositeRetryListener.html[`CompositeRetryListener`]. diff --git a/framework-docs/modules/ROOT/pages/core/resources.adoc b/framework-docs/modules/ROOT/pages/core/resources.adoc index 5fd279ab5c66..ae5b7acdef4d 100644 --- a/framework-docs/modules/ROOT/pages/core/resources.adoc +++ b/framework-docs/modules/ROOT/pages/core/resources.adoc @@ -14,8 +14,6 @@ Spring. It includes the following topics: * xref:core/resources.adoc#resources-app-ctx[Application Contexts and Resource Paths] - - [[resources-introduction]] == Introduction @@ -29,8 +27,6 @@ quite complicated, and the `URL` interface still lacks some desirable functional such as a method to check for the existence of the resource being pointed to. - - [[resources-resource]] == The `Resource` Interface @@ -121,11 +117,8 @@ While this couples your code to Spring, it really only couples it to this small utility classes, which serves as a more capable replacement for `URL` and can be considered equivalent to any other library you would use for this purpose. -NOTE: The `Resource` abstraction does not replace functionality. It wraps it where -possible. For example, a `UrlResource` wraps a URL and uses the wrapped `URL` to do its -work. - - +NOTE: The `Resource` abstraction does not replace functionality. It wraps it where possible. +For example, a `UrlResource` wraps a URL and uses the wrapped `URL` to do its work. [[resources-implementations]] @@ -145,8 +138,6 @@ For a complete list of `Resource` implementations available in Spring, consult t "All Known Implementing Classes" section of the {spring-framework-api}/core/io/Resource.html[`Resource`] javadoc. - - [[resources-implementations-urlresource]] === `UrlResource` @@ -165,8 +156,6 @@ well-known (to property editor, that is) prefix (such as `classpath:`), it creat appropriate specialized `Resource` for that prefix. However, if it does not recognize the prefix, it assumes the string is a standard URL string and creates a `UrlResource`. - - [[resources-implementations-classpathresource]] === `ClassPathResource` @@ -186,8 +175,6 @@ constructor but is often created implicitly when you call an API method that tak `PropertyEditor` recognizes the special prefix, `classpath:`, on the string path and creates a `ClassPathResource` in that case. - - [[resources-implementations-filesystemresource]] === `FileSystemResource` @@ -197,8 +184,6 @@ transformations but performing all operations via the `java.nio.file.Files` API. `java.nio.path.Path` based support use a `PathResource` instead. `FileSystemResource` supports resolution as a `File` and as a `URL`. - - [[resources-implementations-pathresource]] === `PathResource` @@ -208,8 +193,6 @@ as a `URL` and also implements the extended `WritableResource` interface. `PathR is effectively a pure `java.nio.path.Path` based alternative to `FileSystemResource` with different `createRelative` behavior. - - [[resources-implementations-servletcontextresource]] === `ServletContextResource` @@ -222,8 +205,6 @@ filesystem. Whether or not it is expanded and on the filesystem or accessed directly from the JAR or somewhere else like a database (which is conceivable) is actually dependent on the Servlet container. - - [[resources-implementations-inputstreamresource]] === `InputStreamResource` @@ -237,8 +218,6 @@ already-opened resource. Therefore, it returns `true` from `isOpen()`. Do not us you need to keep the resource descriptor somewhere or if you need to read a stream multiple times. - - [[resources-implementations-bytearrayresource]] === `ByteArrayResource` @@ -249,8 +228,6 @@ It is useful for loading content from any given byte array without having to res single-use `InputStreamResource`. - - [[resources-resourceloader]] == The `ResourceLoader` Interface @@ -280,14 +257,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 +286,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 +306,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 +323,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") ---- @@ -385,8 +362,6 @@ objects: |=== - - [[resources-resourcepatternresolver]] == The `ResourcePatternResolver` Interface @@ -436,8 +411,6 @@ implements the `ResourcePatternResolver` interface and delegates to the default ==== - - [[resources-resourceloaderaware]] == The `ResourceLoaderAware` Interface @@ -483,8 +456,6 @@ xref:core/resources.adoc#resources-resourcepatternresolver[`ResourcePatternResol application components instead of `ResourceLoader`. - - [[resources-as-dependencies]] == Resources as Dependencies @@ -505,7 +476,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 +494,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 +542,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 +559,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 +577,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 +594,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[]) @@ -631,16 +602,12 @@ Kotlin:: ====== - - [[resources-app-ctx]] == Application Contexts and Resource Paths This section covers how to create application contexts with resources, including shortcuts that work with XML, how to use wildcards, and other details. - - [[resources-app-ctx-construction]] === Constructing Application Contexts @@ -657,14 +624,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 +644,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 +652,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 +669,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 +677,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") ---- @@ -720,7 +687,6 @@ Using `FileSystemXmlApplicationContext` loads the bean definitions from the clas However, it is still a `FileSystemXmlApplicationContext`. If it is subsequently used as a `ResourceLoader`, any unprefixed paths are still treated as filesystem paths. - [[resources-app-ctx-classpathxml]] ==== Constructing `ClassPathXmlApplicationContext` Instances -- Shortcuts @@ -749,7 +715,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,7 +723,7 @@ 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) ---- @@ -766,8 +732,6 @@ Kotlin:: See the {spring-framework-api}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`] javadoc for details on the various constructors. - - [[resources-app-ctx-wildcards-in-resource-paths]] === Wildcards in Application Context Constructor Resource Paths @@ -788,7 +752,6 @@ resolved at construction time. It has nothing to do with the `Resource` type its You cannot use the `classpath*:` prefix to construct an actual `Resource`, as a resource points to just one resource at a time. - [[resources-app-ctx-ant-patterns-in-paths]] ==== Ant-style Patterns @@ -832,7 +795,6 @@ walk the contents of the jar and resolve the wildcard. This does work in most en but fails in others, and we strongly recommend that the wildcard resolution of resources coming from jars be thoroughly tested in your specific environment before you rely on it. - [[resources-classpath-wildcards]] ==== The `classpath*:` Prefix @@ -843,7 +805,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 +813,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") ---- @@ -880,7 +842,6 @@ used on the last non-wildcard path segment to get all the matching resources in class loader hierarchy and then, off each resource, the same `PathMatcher` resolution strategy described earlier is used for the wildcard subpath. - [[resources-wildcards-in-path-other-stuff]] ==== Other Notes Relating to Wildcards @@ -905,8 +866,8 @@ policies in some environments -- for example, stand-alone applications on JDK 1. and higher (which requires 'Trusted-Library' to be set up in your manifests. See {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. ==== @@ -934,8 +895,6 @@ location found. Therefore, in such cases you should prefer using `classpath*:` w same Ant-style pattern, which searches all classpath locations that contain the `com.mycompany` base package: `classpath*:com/mycompany/**/service-context.xml`. - - [[resources-filesystemresource-caveats]] === `FileSystemResource` Caveats @@ -955,7 +914,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 +922,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 +932,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 +940,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 +953,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 +961,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 +972,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 +980,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 +996,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 +1004,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 +1015,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 +1024,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 67e9d1d31756..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 -{spring-boot-docs}/features.html#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.adoc b/framework-docs/modules/ROOT/pages/core/validation.adoc index cc55989412c0..2f7fe17cc5c1 100644 --- a/framework-docs/modules/ROOT/pages/core/validation.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation.adoc @@ -30,11 +30,8 @@ implementations. They are also discussed in this chapter. Spring supports Java Bean Validation through setup infrastructure and an adaptor to Spring's own `Validator` contract. Applications can enable Bean Validation once globally, -as described in xref:core/validation/beanvalidation.adoc[Java Bean Validation], and use it exclusively for all validation -needs. In the web layer, applications can further register controller-local Spring -`Validator` instances per `DataBinder`, as described in xref:core/validation/beanvalidation.adoc#validation-binder[Configuring a `DataBinder`], which can -be useful for plugging in custom validation logic. - - - - +as described in xref:core/validation/beanvalidation.adoc[Java Bean Validation], and use +it exclusively for all validation needs. In the web layer, applications can further +register controller-local Spring `Validator` instances per `DataBinder`, as described in +xref:core/validation/beanvalidation.adoc#validation-binder[Configuring a `DataBinder`], +which can be useful for plugging in custom validation logic. diff --git a/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc b/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc deleted file mode 100644 index a68dbf43f8ac..000000000000 --- a/framework-docs/modules/ROOT/pages/core/validation/beans-beans.adoc +++ /dev/null @@ -1,705 +0,0 @@ -[[beans-binding]] -= Data Binding - -Data binding is useful for binding user input to a target object where user input is a map -with property paths as keys, following xref:beans-beans-conventions[JavaBeans conventions]. -`DataBinder` is the main class that supports this, and it provides two ways to bind user -input: - -- xref:beans-constructor-binding[Constructor binding] - bind user input to a public data -constructor, looking up constructor argument values in the user input. -- xref:beans-beans[Property binding] - bind user input to setters, matching keys from the -user input to properties of the target object structure. - -You can apply both constructor and property binding or only one. - - -[[beans-constructor-binding]] -== Constructor Binding - -To use constructor binding: - -1. Create a `DataBinder` with `null` as the target object. -2. Set `targetType` to the target class. -3. Call `construct`. - -The target class should have a single public constructor or a single non-public constructor -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. - -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. - -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`. - - - - -[[beans-beans]] -== Property Binding with `BeanWrapper` - -The `org.springframework.beans` package adheres to the JavaBeans standard. -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 -{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 -`BeanWrapper` offers functionality to set and get property values (individually or in -bulk), get property descriptors, and query properties to determine if they are -readable or writable. Also, the `BeanWrapper` offers support for nested properties, -enabling the setting of properties on sub-properties to an unlimited depth. The -`BeanWrapper` also supports the ability to add standard JavaBeans `PropertyChangeListeners` -and `VetoableChangeListeners`, without the need for supporting code in the target class. -Last but not least, the `BeanWrapper` provides support for setting indexed properties. -The `BeanWrapper` usually is not used by application code directly but is used by the -`DataBinder` and the `BeanFactory`. - -The way the `BeanWrapper` works is partly indicated by its name: it wraps a bean to -perform actions on that bean, such as setting and retrieving properties. - - - -[[beans-beans-conventions]] -=== Setting and Getting Basic and Nested Properties - -Setting and getting properties is done through the `setPropertyValue` and -`getPropertyValue` overloaded method variants of `BeanWrapper`. See their Javadoc for -details. The below table shows some examples of these conventions: - -[[beans-beans-conventions-properties-tbl]] -.Examples of properties -|=== -| Expression| Explanation - -| `name` -| Indicates the property `name` that corresponds to the `getName()` or `isName()` - and `setName(..)` methods. - -| `account.name` -| Indicates the nested property `name` of the property `account` that corresponds to - (for example) the `getAccount().setName()` or `getAccount().getName()` methods. - -| `account[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. -|=== - -(This next section is not vitally important to you if you do not plan to work with -the `BeanWrapper` directly. If you use only the `DataBinder` and the `BeanFactory` -and their default implementations, you should skip ahead to the -xref:core/validation/beans-beans.adoc#beans-beans-conversion[section on `PropertyEditors`].) - -The following two example classes use the `BeanWrapper` to get and set -properties: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class Company { - - private String name; - private Employee managingDirector; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public Employee getManagingDirector() { - return this.managingDirector; - } - - public void setManagingDirector(Employee managingDirector) { - this.managingDirector = managingDirector; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class Company { - var name: String? = null - var managingDirector: Employee? = null - } ----- -====== - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class Employee { - - private String name; - - private float salary; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public float getSalary() { - return salary; - } - - public void setSalary(float salary) { - this.salary = salary; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class Employee { - var name: String? = null - var salary: Float? = null - } ----- -====== - -The following code snippets show some examples of how to retrieve and manipulate some of -the properties of instantiated ``Company``s and ``Employee``s: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - BeanWrapper company = new BeanWrapperImpl(new Company()); - // setting the company name.. - company.setPropertyValue("name", "Some Company Inc."); - // ... can also be done like this: - PropertyValue value = new PropertyValue("name", "Some Company Inc."); - company.setPropertyValue(value); - - // ok, let's create the director and tie it to the company: - BeanWrapper jim = new BeanWrapperImpl(new Employee()); - jim.setPropertyValue("name", "Jim Stravinsky"); - company.setPropertyValue("managingDirector", jim.getWrappedInstance()); - - // retrieving the salary of the managingDirector through the company - Float salary = (Float) company.getPropertyValue("managingDirector.salary"); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val company = BeanWrapperImpl(Company()) - // setting the company name.. - company.setPropertyValue("name", "Some Company Inc.") - // ... can also be done like this: - val value = PropertyValue("name", "Some Company Inc.") - company.setPropertyValue(value) - - // ok, let's create the director and tie it to the company: - val jim = BeanWrapperImpl(Employee()) - jim.setPropertyValue("name", "Jim Stravinsky") - company.setPropertyValue("managingDirector", jim.wrappedInstance) - - // retrieving the salary of the managingDirector through the company - val salary = company.getPropertyValue("managingDirector.salary") as Float? ----- -====== - - - -[[beans-beans-conversion]] -== ``PropertyEditor``'s - -Spring uses the concept of a `PropertyEditor` to effect the conversion between an -`Object` and a `String`. It can be handy -to represent properties in a different way than the object itself. For example, a `Date` -can be represented in a human readable way (as the `String`: `'2007-14-09'`), while -we can still convert the human readable form back to the original date (or, even -better, convert any date entered in a human readable form back to `Date` objects). This -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 {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: - -* Setting properties on beans is done by using `PropertyEditor` implementations. - When you use `String` as the value of a property of some bean that you declare - in an XML file, Spring (if the setter of the corresponding property has a `Class` - parameter) uses `ClassEditor` to try to resolve the parameter to a `Class` object. -* Parsing HTTP request parameters in Spring's MVC framework is done by using all kinds - of `PropertyEditor` implementations that you can manually bind in all subclasses of the - `CommandController`. - -Spring has a number of built-in `PropertyEditor` implementations to make life easy. -They are all located in the `org.springframework.beans.propertyeditors` -package. Most, (but not all, as indicated in the following table) are, by default, registered by -`BeanWrapperImpl`. Where the property editor is configurable in some fashion, you can -still register your own variant to override the default one. The following table describes -the various `PropertyEditor` implementations that Spring provides: - -[[beans-beans-property-editors-tbl]] -.Built-in `PropertyEditor` Implementations -[cols="30%,70%"] -|=== -| Class| Explanation - -| `ByteArrayPropertyEditor` -| Editor for byte arrays. Converts strings to their corresponding byte - representations. Registered by default by `BeanWrapperImpl`. - -| `ClassEditor` -| Parses Strings that represent classes to actual classes and vice-versa. When a - class is not found, an `IllegalArgumentException` is thrown. By default, registered by - `BeanWrapperImpl`. - -| `CustomBooleanEditor` -| Customizable property editor for `Boolean` properties. By default, registered by - `BeanWrapperImpl` but can be overridden by registering a custom instance of it as a - custom editor. - -| `CustomCollectionEditor` -| Property editor for collections, converting any source `Collection` to a given target - `Collection` type. - -| `CustomDateEditor` -| Customizable property editor for `java.util.Date`, supporting a custom `DateFormat`. NOT - registered by default. Must be user-registered with the appropriate format as needed. - -| `CustomNumberEditor` -| Customizable property editor for any `Number` subclass, such as `Integer`, `Long`, `Float`, or - `Double`. By default, registered by `BeanWrapperImpl` but can be overridden by - registering a custom instance of it as a custom editor. - -| `FileEditor` -| Resolves strings to `java.io.File` objects. By default, registered by - `BeanWrapperImpl`. - -| `InputStreamEditor` -| One-way property editor that can take a string and produce (through an - intermediate `ResourceEditor` and `Resource`) an `InputStream` so that `InputStream` - properties may be directly set as strings. Note that the default usage does not close - the `InputStream` for you. By default, registered by `BeanWrapperImpl`. - -| `LocaleEditor` -| Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]\_[country]_[variant]`, same as the `toString()` method of - `Locale`). Also accepts spaces as separators, as an alternative to underscores. - By default, registered by `BeanWrapperImpl`. - -| `PatternEditor` -| Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. - -| `PropertiesEditor` -| Can convert strings (formatted with the format defined in the javadoc of the - `java.util.Properties` class) to `Properties` objects. By default, registered - by `BeanWrapperImpl`. - -| `StringTrimmerEditor` -| Property editor that trims strings. Optionally allows transforming an empty string - into a `null` value. NOT registered by default -- must be user-registered. - -| `URLEditor` -| Can resolve a string representation of a URL to an actual `URL` object. - By default, registered by `BeanWrapperImpl`. -|=== - -Spring uses the `java.beans.PropertyEditorManager` to set the search path for property -editors that might be needed. The search path also includes `sun.bean.editors`, which -includes `PropertyEditor` implementations for types such as `Font`, `Color`, and most of -the primitive types. Note also that the standard JavaBeans infrastructure -automatically discovers `PropertyEditor` classes (without you having to register them -explicitly) if they are in the same package as the class they handle and have the same -name as that class, with `Editor` appended. For example, one could have the following -class and package structure, which would be sufficient for the `SomethingEditor` class to be -recognized and used as the `PropertyEditor` for `Something`-typed properties. - -[literal,subs="verbatim,quotes"] ----- -com - chank - pop - Something - SomethingEditor // the PropertyEditor for the Something class ----- - -Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well -(described to some extent -{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: - -[literal,subs="verbatim,quotes"] ----- -com - chank - pop - Something - SomethingBeanInfo // the BeanInfo for the Something class ----- - -The following Java source code for the referenced `SomethingBeanInfo` class -associates a `CustomNumberEditor` with the `age` property of the `Something` class: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class SomethingBeanInfo extends SimpleBeanInfo { - - public PropertyDescriptor[] getPropertyDescriptors() { - try { - final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); - PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { - @Override - public PropertyEditor createPropertyEditor(Object bean) { - return numberPE; - } - }; - return new PropertyDescriptor[] { ageDescriptor }; - } - catch (IntrospectionException ex) { - throw new Error(ex.toString()); - } - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class SomethingBeanInfo : SimpleBeanInfo() { - - override fun getPropertyDescriptors(): Array { - try { - val numberPE = CustomNumberEditor(Int::class.java, true) - val ageDescriptor = object : PropertyDescriptor("age", Something::class.java) { - override fun createPropertyEditor(bean: Any): PropertyEditor { - return numberPE - } - } - return arrayOf(ageDescriptor) - } catch (ex: IntrospectionException) { - throw Error(ex.toString()) - } - - } - } ----- -====== - - -[[beans-beans-conversion-customeditor-registration]] -=== Custom ``PropertyEditor``'s - -When setting bean properties as string values, a Spring IoC container ultimately uses -standard JavaBeans `PropertyEditor` implementations to convert these strings to the complex type of the -property. Spring pre-registers a number of custom `PropertyEditor` implementations (for example, to -convert a class name expressed as a string into a `Class` object). Additionally, -Java's standard JavaBeans `PropertyEditor` lookup mechanism lets a `PropertyEditor` -for a class be named appropriately and placed in the same package as the class -for which it provides support, so that it can be found automatically. - -If there is a need to register other custom `PropertyEditors`, several mechanisms are -available. The most manual approach, which is not normally convenient or -recommended, is to use the `registerCustomEditor()` method of the -`ConfigurableBeanFactory` interface, assuming you have a `BeanFactory` reference. -Another (slightly more convenient) mechanism is to use a special bean factory -post-processor called `CustomEditorConfigurer`. Although you can use bean factory post-processors -with `BeanFactory` implementations, the `CustomEditorConfigurer` has a -nested property setup, so we strongly recommend that you use it with the -`ApplicationContext`, where you can deploy it in similar fashion to any other bean and -where it can be automatically detected and applied. - -Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use of a `BeanWrapper` to -handle property conversions. The standard property editors that the `BeanWrapper` -registers are listed in the xref:core/validation/beans-beans.adoc#beans-beans-conversion[previous section]. -Additionally, ``ApplicationContext``s also override or add additional editors to handle -resource lookups in a manner appropriate to the specific application context type. - -Standard JavaBeans `PropertyEditor` instances are used to convert property values -expressed as strings to the actual complex type of the property. You can use -`CustomEditorConfigurer`, a bean factory post-processor, to conveniently add -support for additional `PropertyEditor` instances to an `ApplicationContext`. - -Consider the following example, which defines a user class called `ExoticType` and -another class called `DependsOnExoticType`, which needs `ExoticType` set as a property: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] ----- - package example; - - public class ExoticType { - - private String name; - - public ExoticType(String name) { - this.name = name; - } - } - - public class DependsOnExoticType { - - private ExoticType type; - - public void setType(ExoticType type) { - this.type = type; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] ----- - package example - - class ExoticType(val name: String) - - class DependsOnExoticType { - - var type: ExoticType? = null - } ----- -====== - -When things are properly set up, we want to be able to assign the type property as a -string, which a `PropertyEditor` converts into an actual -`ExoticType` instance. The following bean definition shows how to set up this relationship: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - ----- - -The `PropertyEditor` implementation could look similar to the following: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] ----- - package example; - - import java.beans.PropertyEditorSupport; - - // converts string representation to ExoticType object - public class ExoticTypeEditor extends PropertyEditorSupport { - - public void setAsText(String text) { - setValue(new ExoticType(text.toUpperCase())); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] ----- - package example - - import java.beans.PropertyEditorSupport - - // converts string representation to ExoticType object - class ExoticTypeEditor : PropertyEditorSupport() { - - override fun setAsText(text: String) { - value = ExoticType(text.toUpperCase()) - } - } ----- -====== - -Finally, the following example shows how to use `CustomEditorConfigurer` to register the new `PropertyEditor` with the -`ApplicationContext`, which will then be able to use it as needed: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - ----- - -[[beans-beans-conversion-customeditor-registration-per]] -=== `PropertyEditorRegistrar` - -Another mechanism for registering property editors with the Spring container is to -create and use a `PropertyEditorRegistrar`. This interface is particularly useful when -you need to use the same set of property editors in several different situations. -You can write a corresponding registrar and reuse it in each case. -`PropertyEditorRegistrar` instances work in conjunction with an interface called -`PropertyEditorRegistry`, an interface that is implemented by the Spring `BeanWrapper` -(and `DataBinder`). `PropertyEditorRegistrar` instances are particularly convenient -when used in conjunction with `CustomEditorConfigurer` (described -xref:core/validation/beans-beans.adoc#beans-beans-conversion-customeditor-registration[here]), which exposes a property -called `setPropertyEditorRegistrars(..)`. `PropertyEditorRegistrar` instances added -to a `CustomEditorConfigurer` in this fashion can easily be shared with `DataBinder` and -Spring MVC controllers. Furthermore, it avoids the need for synchronization on custom -editors: A `PropertyEditorRegistrar` is expected to create fresh `PropertyEditor` -instances for each bean creation attempt. - -The following example shows how to create your own `PropertyEditorRegistrar` implementation: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary",chomp="-packages"] ----- - package com.foo.editors.spring; - - public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar { - - public void registerCustomEditors(PropertyEditorRegistry registry) { - - // it is expected that new PropertyEditor instances are created - registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor()); - - // you could register as many custom property editors as are required here... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",chomp="-packages"] ----- - package com.foo.editors.spring - - import org.springframework.beans.PropertyEditorRegistrar - import org.springframework.beans.PropertyEditorRegistry - - class CustomPropertyEditorRegistrar : PropertyEditorRegistrar { - - override fun registerCustomEditors(registry: PropertyEditorRegistry) { - - // it is expected that new PropertyEditor instances are created - registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor()) - - // you could register as many custom property editors as are required here... - } - } ----- -====== - -See also the `org.springframework.beans.support.ResourceEditorRegistrar` for an example -`PropertyEditorRegistrar` implementation. Notice how in its implementation of the -`registerCustomEditors(..)` method, it creates new instances of each property editor. - -The next example shows how to configure a `CustomEditorConfigurer` and inject an instance -of our `CustomPropertyEditorRegistrar` into it: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - ----- - -Finally (and in a bit of a departure from the focus of this chapter) for those of you -using xref:web/webmvc.adoc#mvc[Spring's MVC web framework], using a `PropertyEditorRegistrar` in -conjunction with data-binding web controllers can be very convenient. The following -example uses a `PropertyEditorRegistrar` in the implementation of an `@InitBinder` method: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Controller - public class RegisterUserController { - - private final PropertyEditorRegistrar customPropertyEditorRegistrar; - - RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { - this.customPropertyEditorRegistrar = propertyEditorRegistrar; - } - - @InitBinder - void initBinder(WebDataBinder binder) { - this.customPropertyEditorRegistrar.registerCustomEditors(binder); - } - - // other methods related to registering a User - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Controller - class RegisterUserController( - private val customPropertyEditorRegistrar: PropertyEditorRegistrar) { - - @InitBinder - fun initBinder(binder: WebDataBinder) { - this.customPropertyEditorRegistrar.registerCustomEditors(binder) - } - - // other methods related to registering a User - } ----- -====== - -This style of `PropertyEditor` registration can lead to concise code (the implementation -of the `@InitBinder` method is only one line long) and lets common `PropertyEditor` -registration code be encapsulated in a class and then shared amongst as many controllers -as needed. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc index b5bf6562dfe2..4a810b98a70c 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc @@ -5,7 +5,6 @@ The Spring Framework provides support for the {bean-validation-site}[Java Bean Validation] API. - [[validation-beanvalidation-overview]] == Overview of Bean Validation @@ -20,7 +19,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 +29,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 +44,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 +59,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) @@ -78,7 +77,6 @@ specific constraints. To learn how to set up a bean validation provider as a Spr bean, keep reading. - [[validation-beanvalidation-spring]] == Configuring a Bean Validation Provider @@ -94,7 +92,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 +108,7 @@ Java:: XML:: + -[source,xml,indent=0,subs="verbatim,quotes",role="secondary"] +[source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -121,7 +119,6 @@ The basic configuration in the preceding example triggers bean validation to ini using its default bootstrap mechanism. A Bean Validation provider, such as the Hibernate Validator, is expected to be present in the classpath and is automatically detected. - [[validation-beanvalidation-spring-inject]] === Inject Jakarta Validator @@ -134,7 +131,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 +145,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import jakarta.validation.Validator; @@ -157,7 +154,6 @@ Kotlin:: ---- ====== - [[validation-beanvalidation-spring-inject-adapter]] === Inject Spring Validator @@ -171,7 +167,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 +181,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,11 +192,9 @@ 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. - - [[validation-beanvalidation-spring-constraints]] === Configure Custom Constraints @@ -226,7 +220,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 +231,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 +244,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 +259,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- import jakarta.validation.ConstraintValidator @@ -276,7 +270,6 @@ Kotlin:: ---- ====== - As the preceding example shows, a `ConstraintValidator` implementation can have its dependencies `@Autowired` as any other Spring bean. @@ -287,32 +280,7 @@ 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 @@ -336,45 +304,15 @@ xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] sections, and the xref:web/webflux/controller/ann-validation.adoc[Validation] and xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] sections. - [[validation-beanvalidation-spring-method-exceptions]] === 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 @@ -384,7 +322,6 @@ fields and properties, the `ParameterValidationResult` is `ParameterErrors` whic implements `org.springframework.validation.Errors` and exposes validation errors as ``FieldError``s. - [[validation-beanvalidation-spring-method-i18n]] === Customizing Validation Errors @@ -398,7 +335,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 +351,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) @@ -431,7 +368,7 @@ Kotlin:: 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" @@ -439,33 +376,32 @@ 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 `MessageSourceResolvable` with error codes -`"student.name"` and `"name"` and can customized too. For example: +`"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} ---- - [[validation-beanvalidation-spring-other]] === Additional Configuration Options @@ -476,7 +412,6 @@ constructs, from message interpolation to traversal resolution. See the javadoc for more information on these options. - [[validation-binder]] == Configuring a `DataBinder` @@ -491,7 +426,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 +444,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) @@ -533,8 +468,7 @@ locally on a DataBinder instance. See xref:web/webmvc/mvc-config/validation.adoc[Spring MVC Validation Configuration]. - [[validation-mvc]] -== Spring MVC 3 Validation +== Spring MVC Validation See xref:web/webmvc/mvc-config/validation.adoc[Validation] in the Spring MVC chapter. diff --git a/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc b/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc deleted file mode 100644 index 37c62169572f..000000000000 --- a/framework-docs/modules/ROOT/pages/core/validation/conversion.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[validation-conversion]] -= Resolving Codes to Error Messages - -We covered databinding and validation. This section covers outputting messages that correspond -to validation errors. In the example shown in the xref:core/validation/validator.adoc[preceding section], -we rejected the `name` and `age` fields. If we want to output the error messages by using a -`MessageSource`, we can do so using the error code we provide when rejecting the field -('name' and 'age' in this case). When you call (either directly, or indirectly, by using, -for example, the `ValidationUtils` class) `rejectValue` or one of the other `reject` methods -from the `Errors` interface, the underlying implementation not only registers the code you -passed in but also registers a number of additional error codes. The `MessageCodesResolver` -determines which error codes the `Errors` interface registers. By default, the -`DefaultMessageCodesResolver` is used, which (for example) not only registers a message -with the code you gave but also registers messages that include the field name you passed -to the reject method. So, if you reject a field by using `rejectValue("age", "too.darn.old")`, -apart from the `too.darn.old` code, Spring also registers `too.darn.old.age` and -`too.darn.old.age.int` (the first includes the field name and the second includes the type -of the field). This is done as a convenience to aid developers when targeting error messages. - -More information on the `MessageCodesResolver` and the default strategy can be found -in the javadoc of -{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..fbaafd2c5384 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/convert.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/convert.adoc @@ -9,7 +9,6 @@ the required property types. You can also use the public API anywhere in your ap where type conversion is needed. - [[core-convert-Converter-API]] == Converter SPI @@ -54,7 +53,6 @@ The following listing shows the `StringToInteger` class, which is a typical `Con ---- - [[core-convert-ConverterFactory-SPI]] == Using `ConverterFactory` @@ -72,9 +70,9 @@ When you need to centralize the conversion logic for an entire class hierarchy } ---- -Parameterize S to be the type you are converting from and R to be the base type defining +Parameterize `S` to be the type you are converting from and `R` to be the base type defining the __range__ of classes you can convert to. Then implement `getConverter(Class)`, -where T is a subclass of R. +where `T` is a subclass of `R`. Consider the `StringToEnumConverterFactory` as an example: @@ -107,13 +105,15 @@ Consider the `StringToEnumConverterFactory` as an example: [[core-convert-GenericConverter-SPI]] == Using `GenericConverter` -When you require a sophisticated `Converter` implementation, consider using the -`GenericConverter` interface. With a more flexible but less strongly typed signature -than `Converter`, a `GenericConverter` supports converting between multiple source and -target types. In addition, a `GenericConverter` makes available source and target field -context that you can use when you implement your conversion logic. Such context lets a -type conversion be driven by a field annotation or by generic information declared on a -field signature. The following listing shows the interface definition of `GenericConverter`: +When you require a more sophisticated `Converter` implementation, consider using the +`GenericConverter` interface. With a more flexible but less strongly typed signature than +`Converter`, a `GenericConverter` supports converting between multiple source and target +types. In addition, a `GenericConverter` is provided source and target type descriptors +that you can use when you implement your conversion logic. Such type descriptors enable +type conversion to be driven by an annotation on the source of the descriptor (such as a +field or method) or by generic information declared in a field signature, method +signature, etc. The following listing shows the definition of the `GenericConverter` +interface: [source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ---- @@ -128,29 +128,29 @@ field signature. The following listing shows the interface definition of `Generi ---- To implement a `GenericConverter`, have `getConvertibleTypes()` return the supported -source->target type pairs. Then implement `convert(Object, TypeDescriptor, +source → target type pairs. Then implement `convert(Object, TypeDescriptor, TypeDescriptor)` to contain your conversion logic. The source `TypeDescriptor` provides -access to the source field that holds the value being converted. The target `TypeDescriptor` -provides access to the target field where the converted value is to be set. +access to the source field or method that holds the value being converted. The target +`TypeDescriptor` provides access to the target field or method where the converted value +is to be set. A good example of a `GenericConverter` is a converter that converts between a Java array -and a collection. Such an `ArrayToCollectionConverter` introspects the field that declares -the target collection type to resolve the collection's element type. This lets each -element in the source array be converted to the collection element type before the -collection is set on the target field. +and a collection. Such an `ArrayToCollectionConverter` introspects the field or method +that declares the target collection type to resolve the collection's element type. This +lets each element in the source array be converted to the collection element type before +the collection is set on the target field or supplied to the target method or constructor. NOTE: Because `GenericConverter` is a more complex SPI interface, you should use it only when you need it. Favor `Converter` or `ConverterFactory` for basic type conversion needs. - [[core-convert-ConditionalGenericConverter-SPI]] === Using `ConditionalGenericConverter` Sometimes, you want a `Converter` to run only if a specific condition holds true. For -example, you might want to run a `Converter` only if a specific annotation is present -on the target field, or you might want to run a `Converter` only if a specific method -(such as a `static valueOf` method) is defined on the target class. +example, you might want to run a `Converter` only if a specific annotation is present on +the target field or method, or you might want to run a `Converter` only if a specific +method (such as a `static valueOf` method) is defined on the target type. `ConditionalGenericConverter` is the union of the `GenericConverter` and `ConditionalConverter` interfaces that lets you define such custom matching criteria: @@ -172,7 +172,6 @@ might match only if the target entity type declares a static finder method (for `matches(TypeDescriptor, TypeDescriptor)`. - [[core-convert-ConversionService-API]] == The `ConversionService` API @@ -205,14 +204,13 @@ use in most environments. `ConversionServiceFactory` provides a convenient facto creating common `ConversionService` configurations. - [[core-convert-Spring-config]] == Configuring a `ConversionService` A `ConversionService` is a stateless object designed to be instantiated at application startup and then shared between multiple threads. In a Spring application, you typically configure a `ConversionService` instance for each Spring container (or `ApplicationContext`). -Spring picks up that `ConversionService` and uses it whenever a type +Spring picks up that `ConversionService` and uses it whenever type conversion needs to be performed by the framework. You can also inject this `ConversionService` into any of your beans and invoke it directly. @@ -249,8 +247,8 @@ It is also common to use a `ConversionService` within a Spring MVC application. xref:web/webmvc/mvc-config/conversion.adoc[Conversion and Formatting] in the Spring MVC chapter. In certain situations, you may wish to apply formatting during conversion. See -xref:core/validation/format.adoc#format-FormatterRegistry-SPI[The `FormatterRegistry` SPI] for details on using `FormattingConversionServiceFactoryBean`. - +xref:core/validation/format.adoc#format-FormatterRegistry-SPI[The `FormatterRegistry` SPI] +for details on using `FormattingConversionServiceFactoryBean`. [[core-convert-programmatic-usage]] @@ -263,7 +261,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 +280,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,27 +304,31 @@ 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(); List input = ... cs.convert(input, - TypeDescriptor.forObject(input), // List type descriptor - TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class))); + TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Integer.class)), // <1> + TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class))); // <2> ---- +<1> `List` type descriptor +<2> `List` type descriptor Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val cs = DefaultConversionService() val input: List = ... cs.convert(input, - TypeDescriptor.forObject(input), // List type descriptor - TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(String::class.java))) + TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(Integer::class.java)), // <1> + TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(String::class.java))) // <2> ---- +<1> `List` type descriptor +<2> `List` type descriptor ====== Note that `DefaultConversionService` automatically registers converters that are @@ -338,7 +340,3 @@ method on the `DefaultConversionService` class. Converters for value types are reused for arrays and collections, so there is no need to create a specific converter to convert from a `Collection` of `S` to a `Collection` of `T`, assuming that standard collection handling is appropriate. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/validation/data-binding.adoc b/framework-docs/modules/ROOT/pages/core/validation/data-binding.adoc new file mode 100644 index 000000000000..3c7831c604f1 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/core/validation/data-binding.adoc @@ -0,0 +1,701 @@ +[[data-binding]] += Data Binding + +Data binding is useful for binding user input to a target object where user input is a map +with property paths as keys, following xref:data-binding-conventions[JavaBeans conventions]. +`DataBinder` is the main class that supports this, and it provides two ways to bind user +input: + +- xref:data-binding-constructor-binding[Constructor binding] - bind user input to a + public data constructor, looking up constructor argument values in the user input. +- xref:data-binding-property-binding[Property binding] - bind user input to setters, + matching keys from the user input to properties of the target object structure. + +You can apply both constructor and property binding or only one. + + +[[data-binding-constructor-binding]] +== Constructor Binding + +To use constructor binding: + +1. Create a `DataBinder` with `null` as the target object. +2. Set `targetType` to the target class. +3. Call `construct`. + +The target class should have a single public constructor or a single non-public constructor +with arguments. If there are multiple constructors, then a default constructor if present +is used. + +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:data-binding-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`. + + +[[data-binding-property-binding]] +== Property Binding with `BeanWrapper` + +The `org.springframework.beans` package adheres to the JavaBeans standard. +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 +{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 +`BeanWrapper` offers functionality to set and get property values (individually or in +bulk), get property descriptors, and query properties to determine if they are +readable or writable. Also, the `BeanWrapper` offers support for nested properties, +enabling the setting of properties on sub-properties to an unlimited depth. The +`BeanWrapper` also supports the ability to add standard JavaBeans `PropertyChangeListeners` +and `VetoableChangeListeners`, without the need for supporting code in the target class. +Last but not least, the `BeanWrapper` provides support for setting indexed properties. +The `BeanWrapper` usually is not used by application code directly but is used by the +`DataBinder` and the `BeanFactory`. + +The way the `BeanWrapper` works is partly indicated by its name: it wraps a bean to +perform actions on that bean, such as setting and retrieving properties. + + +[[data-binding-conventions]] +=== Setting and Getting Basic and Nested Properties + +Setting and getting properties is done through the `setPropertyValue` and +`getPropertyValue` overloaded method variants of `BeanWrapper`. See their Javadoc for +details. The below table shows some examples of these conventions: + +[[data-binding-conventions-properties-tbl]] +.Examples of properties +|=== +| Expression| Explanation + +| `name` +| Indicates the property `name` that corresponds to the `getName()` or `isName()` + and `setName(..)` methods. + +| `account.name` +| Indicates the nested property `name` of the property `account` that corresponds to + (for example) the `getAccount().setName()` or `getAccount().getName()` methods. + +| `accounts[2]` +| Indicates the _third_ element of the indexed property `account`. Indexed properties + can be of type `array`, `list`, or other naturally ordered collection. + +| `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 +the `BeanWrapper` directly. If you use only the `DataBinder` and the `BeanFactory` +and their default implementations, you should skip ahead to the +xref:core/validation/data-binding.adoc#data-binding-conversion[section on `PropertyEditors`].) + +The following two example classes use the `BeanWrapper` to get and set +properties: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class Company { + + private String name; + private Employee managingDirector; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Employee getManagingDirector() { + return this.managingDirector; + } + + public void setManagingDirector(Employee managingDirector) { + this.managingDirector = managingDirector; + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + class Company { + var name: String? = null + var managingDirector: Employee? = null + } +---- +====== + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class Employee { + + private String name; + + private float salary; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public float getSalary() { + return salary; + } + + public void setSalary(float salary) { + this.salary = salary; + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + class Employee { + var name: String? = null + var salary: Float? = null + } +---- +====== + +The following code snippets show some examples of how to retrieve and manipulate some of +the properties of instantiated ``Company``s and ``Employee``s: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + BeanWrapper company = new BeanWrapperImpl(new Company()); + // setting the company name.. + company.setPropertyValue("name", "Some Company Inc."); + // ... can also be done like this: + PropertyValue value = new PropertyValue("name", "Some Company Inc."); + company.setPropertyValue(value); + + // ok, let's create the director and tie it to the company: + BeanWrapper jim = new BeanWrapperImpl(new Employee()); + jim.setPropertyValue("name", "Jim Stravinsky"); + company.setPropertyValue("managingDirector", jim.getWrappedInstance()); + + // retrieving the salary of the managingDirector through the company + Float salary = (Float) company.getPropertyValue("managingDirector.salary"); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val company = BeanWrapperImpl(Company()) + // setting the company name.. + company.setPropertyValue("name", "Some Company Inc.") + // ... can also be done like this: + val value = PropertyValue("name", "Some Company Inc.") + company.setPropertyValue(value) + + // ok, let's create the director and tie it to the company: + val jim = BeanWrapperImpl(Employee()) + jim.setPropertyValue("name", "Jim Stravinsky") + company.setPropertyValue("managingDirector", jim.wrappedInstance) + + // retrieving the salary of the managingDirector through the company + val salary = company.getPropertyValue("managingDirector.salary") as Float? +---- +====== + + +[[data-binding-conversion]] +== ``PropertyEditor``s + +Spring uses the concept of a `PropertyEditor` to effect the conversion between an +`Object` and a `String`. It can be handy +to represent properties in a different way than the object itself. For example, a `Date` +can be represented in a human readable way (as the `String`: `'2007-14-09'`), while +we can still convert the human readable form back to the original date (or, even +better, convert any date entered in a human readable form back to `Date` objects). This +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 {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: + +* Setting properties on beans is done by using `PropertyEditor` implementations. + When you use `String` as the value of a property of some bean that you declare + in an XML file, Spring (if the setter of the corresponding property has a `Class` + parameter) uses `ClassEditor` to try to resolve the parameter to a `Class` object. +* Parsing HTTP request parameters in Spring's MVC framework is done by using all kinds + of `PropertyEditor` implementations that you can manually bind in all subclasses of the + `CommandController`. + +Spring has a number of built-in `PropertyEditor` implementations to make life easy. +They are all located in the `org.springframework.beans.propertyeditors` +package. Most, (but not all, as indicated in the following table) are, by default, registered by +`BeanWrapperImpl`. Where the property editor is configurable in some fashion, you can +still register your own variant to override the default one. The following table describes +the various `PropertyEditor` implementations that Spring provides: + +[[data-binding-property-editors-tbl]] +.Built-in `PropertyEditor` Implementations +[cols="30%,70%"] +|=== +| Class| Explanation + +| `ByteArrayPropertyEditor` +| Editor for byte arrays. Converts strings to their corresponding byte + representations. Registered by default by `BeanWrapperImpl`. + +| `ClassEditor` +| Parses Strings that represent classes to actual classes and vice-versa. When a + class is not found, an `IllegalArgumentException` is thrown. By default, registered by + `BeanWrapperImpl`. + +| `CustomBooleanEditor` +| Customizable property editor for `Boolean` properties. By default, registered by + `BeanWrapperImpl` but can be overridden by registering a custom instance of it as a + custom editor. + +| `CustomCollectionEditor` +| Property editor for collections, converting any source `Collection` to a given target + `Collection` type. + +| `CustomDateEditor` +| Customizable property editor for `java.util.Date`, supporting a custom `DateFormat`. NOT + registered by default. Must be user-registered with the appropriate format as needed. + +| `CustomNumberEditor` +| Customizable property editor for any `Number` subclass, such as `Integer`, `Long`, `Float`, or + `Double`. By default, registered by `BeanWrapperImpl` but can be overridden by + registering a custom instance of it as a custom editor. + +| `FileEditor` +| Resolves strings to `java.io.File` objects. By default, registered by + `BeanWrapperImpl`. + +| `InputStreamEditor` +| One-way property editor that can take a string and produce (through an + intermediate `ResourceEditor` and `Resource`) an `InputStream` so that `InputStream` + properties may be directly set as strings. Note that the default usage does not close + the `InputStream` for you. By default, registered by `BeanWrapperImpl`. + +| `LocaleEditor` +| Can resolve strings to `Locale` objects and vice-versa (the string format is + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. + +| `PatternEditor` +| Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. + +| `PropertiesEditor` +| Can convert strings (formatted with the format defined in the javadoc of the + `java.util.Properties` class) to `Properties` objects. By default, registered + by `BeanWrapperImpl`. + +| `StringTrimmerEditor` +| Property editor that trims strings. Optionally allows transforming an empty string + into a `null` value. NOT registered by default -- must be user-registered. + +| `URLEditor` +| Can resolve a string representation of a URL to an actual `URL` object. + By default, registered by `BeanWrapperImpl`. +|=== + +Spring uses the `java.beans.PropertyEditorManager` to set the search path for property +editors that might be needed. The search path also includes `sun.bean.editors`, which +includes `PropertyEditor` implementations for types such as `Font`, `Color`, and most of +the primitive types. Note also that the standard JavaBeans infrastructure +automatically discovers `PropertyEditor` classes (without you having to register them +explicitly) if they are in the same package as the class they handle and have the same +name as that class, with `Editor` appended. For example, one could have the following +class and package structure, which would be sufficient for the `SomethingEditor` class to be +recognized and used as the `PropertyEditor` for `Something`-typed properties. + +[literal,subs="verbatim,quotes"] +---- +com +└── example + └── things + ├── *Something* + └── *SomethingEditor* // the PropertyEditor for the Something class +---- + +Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well +(described to some extent +{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: + +[literal,subs="verbatim,quotes"] +---- +com +└── example + └── things + ├── *Something* + └── *SomethingBeanInfo* // the BeanInfo for the Something class +---- + +The following Java source code for the referenced `SomethingBeanInfo` class +associates a `CustomNumberEditor` with the `age` property of the `Something` class: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class SomethingBeanInfo extends SimpleBeanInfo { + + public PropertyDescriptor[] getPropertyDescriptors() { + try { + final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); + PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override + public PropertyEditor createPropertyEditor(Object bean) { + return numberPE; + } + }; + return new PropertyDescriptor[] { ageDescriptor }; + } + catch (IntrospectionException ex) { + throw new Error(ex.toString()); + } + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + class SomethingBeanInfo : SimpleBeanInfo() { + + override fun getPropertyDescriptors(): Array { + try { + val numberPE = CustomNumberEditor(Int::class.java, true) + val ageDescriptor = object : PropertyDescriptor("age", Something::class.java) { + override fun createPropertyEditor(bean: Any): PropertyEditor { + return numberPE + } + } + return arrayOf(ageDescriptor) + } catch (ex: IntrospectionException) { + throw Error(ex.toString()) + } + + } + } +---- +====== + + +[[data-binding-conversion-customeditor-registration]] +=== Custom ``PropertyEditor``s + +When setting bean properties as string values, a Spring IoC container ultimately uses +standard JavaBeans `PropertyEditor` implementations to convert these strings to the complex type of the +property. Spring pre-registers a number of custom `PropertyEditor` implementations (for example, to +convert a class name expressed as a string into a `Class` object). Additionally, +Java's standard JavaBeans `PropertyEditor` lookup mechanism lets a `PropertyEditor` +for a class be named appropriately and placed in the same package as the class +for which it provides support, so that it can be found automatically. + +If there is a need to register other custom `PropertyEditors`, several mechanisms are +available. The most manual approach, which is not normally convenient or +recommended, is to use the `registerCustomEditor()` method of the +`ConfigurableBeanFactory` interface, assuming you have a `BeanFactory` reference. +Another (slightly more convenient) mechanism is to use a special bean factory +post-processor called `CustomEditorConfigurer`. Although you can use bean factory post-processors +with `BeanFactory` implementations, the `CustomEditorConfigurer` has a +nested property setup, so we strongly recommend that you use it with the +`ApplicationContext`, where you can deploy it in similar fashion to any other bean and +where it can be automatically detected and applied. + +Note that all bean factories and application contexts automatically use a number of +built-in property editors, through their use of a `BeanWrapper` to +handle property conversions. The standard property editors that the `BeanWrapper` +registers are listed in the xref:core/validation/data-binding.adoc#data-binding-conversion[previous section]. +Additionally, ``ApplicationContext``s also override or add additional editors to handle +resource lookups in a manner appropriate to the specific application context type. + +Standard JavaBeans `PropertyEditor` instances are used to convert property values +expressed as strings to the actual complex type of the property. You can use +`CustomEditorConfigurer`, a bean factory post-processor, to conveniently add +support for additional `PropertyEditor` instances to an `ApplicationContext`. + +Consider the following example, which defines a user class called `ExoticType` and +another class called `DependsOnExoticType`, which needs `ExoticType` set as a property: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] +---- + package example; + + public class ExoticType { + + private String name; + + public ExoticType(String name) { + this.name = name; + } + } + + public class DependsOnExoticType { + + private ExoticType type; + + public void setType(ExoticType type) { + this.type = type; + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] +---- + package example + + class ExoticType(val name: String) + + class DependsOnExoticType { + + var type: ExoticType? = null + } +---- +====== + +When things are properly set up, we want to be able to assign the type property as a +string, which a `PropertyEditor` converts into an actual +`ExoticType` instance. The following bean definition shows how to set up this relationship: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + +---- + +The `PropertyEditor` implementation could look similar to the following: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] +---- + package example; + + import java.beans.PropertyEditorSupport; + + // converts string representation to ExoticType object + public class ExoticTypeEditor extends PropertyEditorSupport { + + public void setAsText(String text) { + setValue(new ExoticType(text.toUpperCase())); + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] +---- + package example + + import java.beans.PropertyEditorSupport + + // converts string representation to ExoticType object + class ExoticTypeEditor : PropertyEditorSupport() { + + override fun setAsText(text: String) { + value = ExoticType(text.toUpperCase()) + } + } +---- +====== + +Finally, the following example shows how to use `CustomEditorConfigurer` to register the new `PropertyEditor` with the +`ApplicationContext`, which will then be able to use it as needed: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + +---- + +[[data-binding-conversion-customeditor-registration-per]] +=== `PropertyEditorRegistrar` + +Another mechanism for registering property editors with the Spring container is to +create and use a `PropertyEditorRegistrar`. This interface is particularly useful when +you need to use the same set of property editors in several different situations. +You can write a corresponding registrar and reuse it in each case. +`PropertyEditorRegistrar` instances work in conjunction with an interface called +`PropertyEditorRegistry`, an interface that is implemented by the Spring `BeanWrapper` +(and `DataBinder`). `PropertyEditorRegistrar` instances are particularly convenient +when used in conjunction with `CustomEditorConfigurer` (described +xref:core/validation/data-binding.adoc#data-binding-conversion-customeditor-registration[here]), which exposes a property +called `setPropertyEditorRegistrars(..)`. `PropertyEditorRegistrar` instances added +to a `CustomEditorConfigurer` in this fashion can easily be shared with `DataBinder` and +Spring MVC controllers. Furthermore, it avoids the need for synchronization on custom +editors: A `PropertyEditorRegistrar` is expected to create fresh `PropertyEditor` +instances for each bean creation attempt. + +The following example shows how to create your own `PropertyEditorRegistrar` implementation: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] +---- + package com.foo.editors.spring; + + public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar { + + public void registerCustomEditors(PropertyEditorRegistry registry) { + + // it is expected that new PropertyEditor instances are created + registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor()); + + // you could register as many custom property editors as are required here... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"] +---- + package com.foo.editors.spring + + import org.springframework.beans.PropertyEditorRegistrar + import org.springframework.beans.PropertyEditorRegistry + + class CustomPropertyEditorRegistrar : PropertyEditorRegistrar { + + override fun registerCustomEditors(registry: PropertyEditorRegistry) { + + // it is expected that new PropertyEditor instances are created + registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor()) + + // you could register as many custom property editors as are required here... + } + } +---- +====== + +See also the `org.springframework.beans.support.ResourceEditorRegistrar` for an example +`PropertyEditorRegistrar` implementation. Notice how in its implementation of the +`registerCustomEditors(..)` method, it creates new instances of each property editor. + +The next example shows how to configure a `CustomEditorConfigurer` and inject an instance +of our `CustomPropertyEditorRegistrar` into it: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + + + + + + + + +---- + +Finally (and in a bit of a departure from the focus of this chapter) for those of you +using xref:web/webmvc.adoc#mvc[Spring's MVC web framework], using a `PropertyEditorRegistrar` in +conjunction with data-binding web controllers can be very convenient. The following +example uses a `PropertyEditorRegistrar` in the implementation of an `@InitBinder` method: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class RegisterUserController { + + private final PropertyEditorRegistrar customPropertyEditorRegistrar; + + RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { + this.customPropertyEditorRegistrar = propertyEditorRegistrar; + } + + @InitBinder + void initBinder(WebDataBinder binder) { + this.customPropertyEditorRegistrar.registerCustomEditors(binder); + } + + // other methods related to registering a User + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Controller + class RegisterUserController( + private val customPropertyEditorRegistrar: PropertyEditorRegistrar) { + + @InitBinder + fun initBinder(binder: WebDataBinder) { + this.customPropertyEditorRegistrar.registerCustomEditors(binder) + } + + // other methods related to registering a User + } +---- +====== + +This style of `PropertyEditor` registration can lead to concise code (the implementation +of the `@InitBinder` method is only one line long) and lets common `PropertyEditor` +registration code be encapsulated in a class and then shared amongst as many controllers +as needed. diff --git a/framework-docs/modules/ROOT/pages/core/validation/error-code-resolution.adoc b/framework-docs/modules/ROOT/pages/core/validation/error-code-resolution.adoc new file mode 100644 index 000000000000..5bf1765474f3 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/core/validation/error-code-resolution.adoc @@ -0,0 +1,24 @@ +[[validation-error-code-resolution]] += Resolving Error Codes to Error Messages + +We covered data binding and validation. This section covers outputting messages that correspond +to validation errors. In the example shown in the xref:core/validation/validator.adoc[preceding section], +we rejected the `name` and `age` fields. If we want to output the error messages by using a +`MessageSource`, we can do so using the error code we provide when rejecting the field +('name' and 'age' in this case). When you call (either directly, or indirectly, by using, +for example, the `ValidationUtils` class) `rejectValue` or one of the other `reject` methods +from the `Errors` interface, the underlying implementation not only registers the code you +passed in but also registers a number of additional error codes. The `MessageCodesResolver` +determines which error codes the `Errors` interface registers. By default, the +`DefaultMessageCodesResolver` is used, which (for example) not only registers a message +with the code you gave but also registers messages that include the field name you passed +to the reject method. So, if you reject a field by using `rejectValue("age", "too.darn.old")`, +apart from the `too.darn.old` code, Spring also registers `too.darn.old.age` and +`too.darn.old.age.int` (the first includes the field name and the second includes the type +of the field). This is done as a convenience to aid developers when targeting error messages. + +More information on the `MessageCodesResolver` and the default strategy can be found +in the javadoc of +{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/format-configuring-formatting-globaldatetimeformat.adoc b/framework-docs/modules/ROOT/pages/core/validation/format-configuring-formatting-globaldatetimeformat.adoc index 1b67d8467a1c..f14380caee56 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 @@ -19,6 +19,3 @@ Note there are extra considerations when configuring date and time formats in we applications. Please see xref:web/webmvc/mvc-config/conversion.adoc[WebMVC Conversion and Formatting] or xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux Conversion and Formatting]. - - - diff --git a/framework-docs/modules/ROOT/pages/core/validation/format.adoc b/framework-docs/modules/ROOT/pages/core/validation/format.adoc index 4ac313d3f298..1ab37bdc15ab 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/format.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/format.adoc @@ -26,7 +26,6 @@ application) and need to parse and print localized field values. The `Conversion provides a unified type conversion API for both SPIs. - [[format-Formatter-SPI]] == The `Formatter` SPI @@ -74,7 +73,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 +82,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 +118,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 { @@ -142,7 +142,6 @@ The Spring team welcomes community-driven `Formatter` contributions. See {spring-framework-issues}[GitHub Issues] to contribute. - [[format-CustomFormatAnnotations]] == Annotation-driven Formatting @@ -179,7 +178,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 +215,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class NumberFormatAnnotationFormatterFactory : AnnotationFormatterFactory { @@ -255,7 +254,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 +265,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 @@ -274,23 +273,23 @@ Kotlin:: ---- ====== - [[format-annotations-api]] === Format Annotation API 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 +300,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 +308,30 @@ 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 +339,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). @@ -351,7 +374,6 @@ annotation are formatted in a certain way. With a shared `FormatterRegistry`, yo these rules once, and they are applied whenever formatting is needed. - [[format-FormatterRegistrar-SPI]] == The `FormatterRegistrar` SPI @@ -376,12 +398,7 @@ registering a `Printer`/`Parser` pair. The next section provides more informatio converter and formatter registration. - [[format-configuring-formatting-mvc]] == Configuring Formatting in Spring MVC See xref:web/webmvc/mvc-config/conversion.adoc[Conversion and Formatting] in the Spring MVC chapter. - - - - diff --git a/framework-docs/modules/ROOT/pages/core/validation/validator.adoc b/framework-docs/modules/ROOT/pages/core/validation/validator.adoc index 17fa6402d3ba..b73122c2d800 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/validator.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/validator.adoc @@ -1,5 +1,5 @@ [[validator]] -= Validation by Using Spring's Validator Interface += Validation Using Spring's Validator Interface Spring features a `Validator` interface that you can use to validate objects. The `Validator` interface works by using an `Errors` object so that, while validating, @@ -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 { @@ -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 { @@ -198,9 +198,6 @@ methods it offers can be found in the {spring-framework-api}/validation/Errors.h 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.adoc b/framework-docs/modules/ROOT/pages/data-access.adoc index df232e910faf..a1db0cf1137d 100644 --- a/framework-docs/modules/ROOT/pages/data-access.adoc +++ b/framework-docs/modules/ROOT/pages/data-access.adoc @@ -8,7 +8,3 @@ interaction between the data access layer and the business or service layer. Spring's comprehensive transaction management support is covered in some detail, followed by thorough coverage of the various data access frameworks and technologies with which the Spring Framework integrates. - - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/appendix.adoc b/framework-docs/modules/ROOT/pages/data-access/appendix.adoc index 4374af3b7e26..db268abfbbe3 100644 --- a/framework-docs/modules/ROOT/pages/data-access/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/appendix.adoc @@ -2,8 +2,6 @@ = Appendix - - [[xsd-schemas]] == XML Schemas @@ -12,8 +10,6 @@ This part of the appendix lists XML schemas for data access, including the follo * xref:data-access/appendix.adoc#xsd-schemas-tx[The `tx` Schema] * xref:data-access/appendix.adoc#xsd-schemas-jdbc[The `jdbc` Schema] - - [[xsd-schemas-tx]] === The `tx` Schema @@ -61,8 +57,6 @@ implemented by using AOP). The preceding XML snippet contains the relevant lines to reference the `aop` schema so that the elements in the `aop` namespace are available to you. - - [[xsd-schemas-jdbc]] === The `jdbc` Schema diff --git a/framework-docs/modules/ROOT/pages/data-access/dao.adoc b/framework-docs/modules/ROOT/pages/data-access/dao.adoc index 9ca9666f5415..4ed57c4a2013 100644 --- a/framework-docs/modules/ROOT/pages/data-access/dao.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/dao.adoc @@ -8,7 +8,6 @@ and it also lets you code without worrying about catching exceptions that are specific to each technology. - [[dao-exceptions]] == Consistent Exception Hierarchy @@ -42,7 +41,6 @@ The following image shows the exception hierarchy that Spring provides. image::DataAccessException.png[] - [[dao-annotations]] == Annotations Used to Configure DAO or Repository Classes @@ -56,7 +54,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 +65,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 +87,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 +101,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 +122,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 +140,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 +158,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 +176,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 { @@ -192,7 +190,3 @@ Kotlin:: NOTE: See the specific coverage of each persistence technology for details on how to configure the application context to take advantage of these annotations. - - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc.adoc index 0f82cea1b648..069e6a7849d8 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc.adoc @@ -53,6 +53,3 @@ takes care of and which actions are your responsibility. The Spring Framework takes care of all the low-level details that can make JDBC such a tedious API. - - - 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..ee48bda1d597 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 { @@ -303,6 +312,3 @@ each batch should be the batch size provided for all batches (except that the la that might be less), depending on the total number of update objects provided. The update count for each update statement is the one reported by the JDBC driver. If the count is not available, the JDBC driver returns a value of `-2`. - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/choose-style.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/choose-style.adoc index 863b8f1cdac4..e588c8254482 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/choose-style.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/choose-style.adoc @@ -22,6 +22,3 @@ and match to include a feature from a different approach. data-access layer. This approach allows you to define your query string, declare parameters, and compile the query. Once you do that, `execute(...)`, `update(...)`, and `findObject(...)` methods can be called multiple times with various parameter values. - - - 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 ddd2103ec673..ca8b107ccd1b 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc @@ -62,6 +62,7 @@ The following example shows C3P0 configuration: include-code::./ComboPooledDataSourceConfiguration[tag=snippet,indent=0] + [[jdbc-DataSourceUtils]] == Using `DataSourceUtils` @@ -177,7 +178,7 @@ corresponding `DataSource` proxy class for the target connection pool: see 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 (e.g. `IsolationLevelDataSourceRouter`). +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 @@ -196,6 +197,3 @@ In terms of exception behavior, `JdbcTransactionManager` is roughly equivalent t `JpaTransactionManager` and also to `R2dbcTransactionManager`, serving as an immediate companion/replacement for each other. `DataSourceTransactionManager` on the other hand is equivalent to `JtaTransactionManager` and can serve as a direct replacement there. - - - 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 8755cb86dafe..8dd2442558ae 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc @@ -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(?)", @@ -398,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; @@ -416,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) @@ -443,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; @@ -461,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) @@ -494,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 { @@ -520,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) ---- @@ -533,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; @@ -552,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) @@ -676,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(); ---- @@ -746,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 { @@ -761,7 +761,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() { @@ -786,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; @@ -811,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 { @@ -846,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; @@ -867,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 @@ -897,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; @@ -922,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 @@ -950,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; @@ -965,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) @@ -992,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; @@ -1013,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 @@ -1051,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"; @@ -1068,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" @@ -1081,6 +1081,3 @@ Kotlin:: // keyHolder.getKey() now contains the generated key ---- ====== - - - 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 c011b168f36b..9090a4b17d58 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 @@ -76,35 +76,35 @@ to customize them if necessary. The following example uses H2 with a custom driv ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - @Configuration - public class DataSourceConfig { - - @Bean - public DataSource dataSource() { - return new EmbeddedDatabaseBuilder() - .setDatabaseConfigurer(EmbeddedDatabaseConfigurers - .customizeConfigurer(H2, this::customize)) - .addScript("schema.sql") - .build(); - } - - private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) { - return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { - @Override - public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { - super.configureConnectionProperties(properties, databaseName); - properties.setDriverClass(CustomDriver.class); - } - }; - } + @Configuration + public class DataSourceConfig { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setDatabaseConfigurer(EmbeddedDatabaseConfigurers + .customizeConfigurer(H2, this::customize)) + .addScript("schema.sql") + .build(); + } + + private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) { + return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, databaseName); + properties.setDriverClass(CustomDriver.class); + } + }; + } } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class DataSourceConfig { @@ -150,7 +150,7 @@ The following listing shows the test template: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class DataAccessIntegrationTestTemplate { @@ -182,7 +182,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class DataAccessIntegrationTestTemplate { @@ -255,6 +255,3 @@ You can extend Spring JDBC embedded database support in two ways: We encourage you to contribute extensions to the Spring community at {spring-framework-issues}[GitHub Issues]. - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/initializing-datasource.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/initializing-datasource.adoc index c17999835667..621f1421a132 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/initializing-datasource.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/initializing-datasource.adoc @@ -141,6 +141,3 @@ Ensuring that the database initializer is initialized first can also be easy. So parent context contains the `DataSource`, and the child context contains the business components). This structure is common in Spring web applications but can be more generally applied. - - - 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..65b1770215d7 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 @@ -599,6 +599,3 @@ Kotlin:: } ---- ====== - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/packages.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/packages.adoc index f2b739def9b4..d55ca6c955a9 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/packages.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/packages.adoc @@ -8,7 +8,9 @@ and its various callback interfaces, plus a variety of related classes. A subpac named `org.springframework.jdbc.core.simple` contains the `SimpleJdbcInsert` and `SimpleJdbcCall` classes. Another subpackage named `org.springframework.jdbc.core.namedparam` contains the `NamedParameterJdbcTemplate` -class and the related support classes. See xref:data-access/jdbc/core.adoc[Using the JDBC Core Classes to Control Basic JDBC Processing and Error Handling], xref:data-access/jdbc/advanced.adoc[JDBC Batch Operations], and +class and the related support classes. See +xref:data-access/jdbc/core.adoc[Using the JDBC Core Classes to Control Basic JDBC Processing and Error Handling], +xref:data-access/jdbc/advanced.adoc[JDBC Batch Operations], and xref:data-access/jdbc/simple.adoc[Simplifying JDBC Operations with the `SimpleJdbc` Classes]. * `datasource`: The `org.springframework.jdbc.datasource` package contains a utility class @@ -16,7 +18,8 @@ for easy `DataSource` access and various simple `DataSource` implementations tha use for testing and running unmodified JDBC code outside of a Jakarta EE container. A subpackage named `org.springframework.jdbc.datasource.embedded` provides support for creating embedded databases by using Java database engines, such as HSQL, H2, and Derby. See -xref:data-access/jdbc/connections.adoc[Controlling Database Connections] and xref:data-access/jdbc/embedded-database-support.adoc[Embedded Database Support]. +xref:data-access/jdbc/connections.adoc[Controlling Database Connections] and +xref:data-access/jdbc/embedded-database-support.adoc[Embedded Database Support]. * `object`: The `org.springframework.jdbc.object` package contains classes that represent RDBMS queries, updates, and stored procedures as thread-safe, reusable objects. See @@ -31,7 +34,5 @@ are translated to exceptions defined in the `org.springframework.dao` package. T that code using the Spring JDBC abstraction layer does not need to implement JDBC or RDBMS-specific error handling. All translated exceptions are unchecked, which gives you the option of catching the exceptions from which you can recover while letting other -exceptions be propagated to the caller. See xref:data-access/jdbc/core.adoc#jdbc-SQLExceptionTranslator[Using `SQLExceptionTranslator`]. - - - +exceptions be propagated to the caller. See +xref:data-access/jdbc/core.adoc#jdbc-SQLExceptionTranslator[Using `SQLExceptionTranslator`]. 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..b2809957bafc 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,25 @@ 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: +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: -[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; - } - }; ----- - -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..4b1823ace27b 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/simple.adoc @@ -15,7 +15,7 @@ configuration options. You should instantiate the `SimpleJdbcInsert` in the data layer's initialization method. For this example, the initializing method is the `setDataSource` method. You do not need to subclass the `SimpleJdbcInsert` class. Instead, you can create a new instance and set the table name by using the `withTableName` method. -Configuration methods for this class follow the `fluid` style that returns the instance +Configuration methods for this class follow the `fluent` style that returns the instance of the `SimpleJdbcInsert`, which lets you chain all configuration methods. The following example uses only one configuration method (we show examples of multiple methods later): @@ -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"] ---- @@ -349,17 +349,17 @@ parameters return the data read from the table. You can declare `SimpleJdbcCall` in a manner similar to declaring `SimpleJdbcInsert`. You should instantiate and configure the class in the initialization method of your data-access -layer. Compared to the `StoredProcedure` class, you need not create a subclass -and you need not to declare parameters that can be looked up in the database metadata. -The following example of a `SimpleJdbcCall` configuration uses the preceding stored -procedure (the only configuration option, in addition to the `DataSource`, is the name -of the stored procedure): +layer. In contrast to the `StoredProcedure` class, you do not need to create a subclass, +and you do not need to declare parameters that can be looked up in the database metadata. +The following `SimpleJdbcCall` configuration example uses the preceding stored procedure. +The only configuration option (other than the `DataSource`) is the name of the stored +procedure. [tabs] ====== 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 { @@ -768,6 +768,3 @@ Kotlin:: The `execute` call passes in an empty `Map`, because this call does not take any parameters. The list of actors is then retrieved from the results map and returned to the caller. - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/orm.adoc b/framework-docs/modules/ROOT/pages/data-access/orm.adoc index c4f0acb9c867..8e445b17041a 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm.adoc @@ -3,6 +3,3 @@ :page-section-summary-toc: 1 This section covers data access when you use Object Relational Mapping (ORM). - - - 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..49ceefcbad6b 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 { @@ -109,6 +109,3 @@ In summary, you can implement DAOs based on the plain persistence technology's A annotations while still benefiting from Spring-managed transactions, dependency injection, and transparent exception conversion (if desired) to Spring's custom exception hierarchies. - - - 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 45328b85cd86..c55b5efcb305 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc @@ -1,7 +1,7 @@ [[orm-hibernate]] = Hibernate -We start with a coverage of https://hibernate.org/[Hibernate 5] in a Spring environment, +We start with a coverage of https://hibernate.org/[Hibernate] in a Spring environment, using it to demonstrate the approach that Spring takes towards integrating OR mappers. This section covers many issues in detail and shows different variations of DAO implementations and transaction demarcation. Most of these patterns can be directly @@ -10,13 +10,12 @@ cover the other ORM technologies and show brief examples. [NOTE] ==== -As of Spring Framework 6.0, Spring requires Hibernate ORM 5.5+ for Spring's -`HibernateJpaVendorAdapter` as well as for a native Hibernate `SessionFactory` setup. -We recommend Hibernate ORM 5.6 as the last feature branch in that Hibernate generation. +As of Spring Framework 7.0, Spring requires Hibernate ORM 7.x for Spring's +`HibernateJpaVendorAdapter`. -Hibernate ORM 6.x is only supported as a JPA provider (`HibernateJpaVendorAdapter`). -Plain `SessionFactory` setup with the `orm.hibernate5` package is not supported anymore. -We recommend Hibernate ORM 6.1/6.2 with JPA-style setup for new development projects. +The `org.springframework.orm.jpa.hibernate` package supersedes the former `orm.hibernate5`: +now for use with Hibernate ORM 7.x, tightly integrated with `HibernateJpaVendorAdapter` +as well as supporting Hibernate's native `SessionFactory.getCurrentSession()` style. ==== @@ -43,7 +42,7 @@ JDBC `DataSource` and a Hibernate `SessionFactory` on top of it: - + @@ -87,8 +86,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 +104,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 +125,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 +207,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 +232,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 { @@ -271,7 +270,7 @@ processing at runtime. The following example shows how to do so: + class="org.springframework.orm.jpa.hibernate.HibernateTransactionManager"> @@ -301,7 +300,7 @@ and an example for a business method implementation: ---- - + @@ -317,7 +316,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 +344,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 { @@ -515,6 +514,3 @@ the following events occur when a JTA transaction commits: * Hibernate is synchronized to the JTA transaction, so the transaction is called back through an `afterCompletion` callback by the JTA transaction manager and can properly clear its cache. - - - 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 d44aca0c20d7..e33a44af7aa5 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/introduction.adoc @@ -48,7 +48,8 @@ The benefits of using the Spring Framework to create your ORM DAOs include: aspect-oriented programming (AOP) style method interceptor either through the `@Transactional` annotation or by explicitly configuring the transaction AOP advice in an XML configuration file. In both cases, transaction semantics and exception handling - (rollback and so on) are handled for you. As discussed in xref:data-access/orm/general.adoc#orm-resource-mngmnt[Resource and Transaction Management], + (rollback and so on) are handled for you. As discussed in + xref:data-access/orm/general.adoc#orm-resource-mngmnt[Resource and Transaction Management], you can also swap various transaction managers, without affecting your ORM-related code. For example, you can swap between local transactions and JTA, with the same full services (such as declarative transactions) available in both scenarios. Additionally, @@ -61,6 +62,3 @@ technologies such as MongoDB, you might want to check out the {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 c0885e226329..8843a46b727a 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? **** @@ -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"] @@ -299,7 +298,7 @@ shows a plain JPA DAO implementation that uses the `@PersistenceUnit` annotation ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public class ProductDaoImpl implements ProductDao { @@ -328,7 +327,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ProductDaoImpl : ProductDao { @@ -394,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 { @@ -411,7 +410,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ProductDaoImpl : ProductDao { @@ -469,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 @@ -628,7 +627,3 @@ On `LocalSessionFactoryBean`, this is available through the `bootstrapExecutor` property. On the programmatic `LocalSessionFactoryBuilder`, an overloaded `buildSessionFactory` method takes a bootstrap executor argument. ==== - - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/oxm.adoc b/framework-docs/modules/ROOT/pages/data-access/oxm.adoc index 5bc8fcdfda96..e2914c19e68c 100644 --- a/framework-docs/modules/ROOT/pages/data-access/oxm.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/oxm.adoc @@ -2,7 +2,6 @@ = Marshalling XML by Using Object-XML Mappers - [[oxm-introduction]] == Introduction @@ -22,7 +21,6 @@ Some of the benefits of using Spring for your O/X mapping needs are: * xref:data-access/oxm.adoc#oxm-consistent-interfaces[Consistent Interfaces] * xref:data-access/oxm.adoc#oxm-consistent-exception-hierarchy[Consistent Exception Hierarchy] - [[oxm-ease-of-configuration]] === Ease of configuration @@ -32,7 +30,6 @@ as you would any other bean in your application context. Additionally, XML names configuration is available for a number of marshallers, making the configuration even simpler. - [[oxm-consistent-interfaces]] === Consistent Interfaces @@ -44,7 +41,6 @@ marshalling with a mix-and-match approach (for example, some marshalling perform and some by XStream) in a non-intrusive fashion, letting you use the strength of each technology. - [[oxm-consistent-exception-hierarchy]] === Consistent Exception Hierarchy @@ -53,7 +49,6 @@ own exception hierarchy with the `XmlMappingException` as the root exception. These runtime exceptions wrap the original exception so that no information is lost. - [[oxm-marshaller-unmarshaller]] == `Marshaller` and `Unmarshaller` @@ -61,7 +56,6 @@ As stated in the xref:data-access/oxm.adoc#oxm-introduction[introduction], a mar to XML, and an unmarshaller deserializes XML stream to an object. This section describes the two Spring interfaces used for this purpose. - [[oxm-marshaller]] === Understanding `Marshaller` @@ -104,7 +98,6 @@ must be mapped in a mapping file, be marked with an annotation, be registered wi marshaller, or have a common base class. Refer to the later sections in this chapter to determine how your O-X technology manages this. - [[oxm-unmarshaller]] === Understanding `Unmarshaller` @@ -146,7 +139,6 @@ Even though there are two separate marshalling interfaces (`Marshaller` and This means that you can wire up one marshaller class and refer to it both as a marshaller and as an unmarshaller in your `applicationContext.xml`. - [[oxm-xmlmappingexception]] === Understanding `XmlMappingException` @@ -163,7 +155,6 @@ The O-X Mapping exception hierarchy is shown in the following figure: image::oxm-exceptions.png[] - [[oxm-usage]] == Using `Marshaller` and `Unmarshaller` @@ -175,7 +166,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 +184,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 +201,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 +252,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Application { @@ -319,7 +310,6 @@ This sample application produces the following `settings.xml` file: ---- - [[oxm-schema-based-config]] == XML Configuration Namespace @@ -356,7 +346,6 @@ the configuration of a JAXB2 marshaller might resemble the following: ---- - [[oxm-jaxb]] == JAXB @@ -369,7 +358,6 @@ Spring supports the JAXB 2.0 API as XML marshalling strategies, following the The corresponding integration classes reside in the `org.springframework.oxm.jaxb` package. - [[oxm-jaxb2]] === Using `Jaxb2Marshaller` @@ -436,7 +424,6 @@ The following table describes the available attributes: |=== - [[oxm-jibx]] == JiBX @@ -450,7 +437,6 @@ For more information on JiBX, see the http://jibx.sourceforge.net/[JiBX web site]. The Spring integration classes reside in the `org.springframework.oxm.jibx` package. - [[oxm-jibx-marshaller]] === Using `JibxMarshaller` @@ -503,7 +489,6 @@ The following table describes the available attributes: |=== - [[oxm-xstream]] == XStream @@ -514,7 +499,6 @@ For more information on XStream, see the https://x-stream.github.io/[XStream web site]. The Spring integration classes reside in the `org.springframework.oxm.xstream` package. - [[oxm-xstream-marshaller]] === Using `XStreamMarshaller` @@ -557,16 +541,14 @@ 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 -{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 -result, default XStream converters with lower priorities and possible security +{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 result, default XStream converters with lower priorities and possible security vulnerabilities do not get invoked. ===== NOTE: Note that XStream is an XML serialization library, not a data binding library. Therefore, it has limited namespace support. As a result, it is rather unsuitable for usage within Web Services. - - diff --git a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc index 672f396ef0f9..4b08f473ee13 100644 --- a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc @@ -33,7 +33,6 @@ including error handling. It includes the following topics: * xref:data-access/r2dbc.adoc#r2dbc-DatabaseClient-filter[Statement Filters] * xref:data-access/r2dbc.adoc#r2dbc-auto-generated-keys[Retrieving Auto-generated Keys] - [[r2dbc-DatabaseClient]] === Using `DatabaseClient` @@ -68,14 +67,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) ---- @@ -133,18 +132,18 @@ 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(); + .then(); ---- 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() + .await() ---- ====== @@ -170,18 +169,18 @@ 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(); + .fetch().first(); ---- 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() + .fetch().awaitSingle() ---- ====== @@ -191,20 +190,20 @@ 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") - .fetch().first(); + .bind("fn", "Joe") + .fetch().first(); ---- 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") - .fetch().awaitSingle() + .bind("fn", "Joe") + .fetch().awaitSingle() ---- ====== @@ -237,20 +236,20 @@ 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)) - .all(); + .map(row -> row.get("name", String.class)) + .all(); ---- 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) } - .flow() + .map{ row: Row -> row.get("name", String.class) } + .flow() ---- ====== @@ -298,20 +297,20 @@ 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") - .fetch().rowsUpdated(); + .bind("fn", "Joe") + .fetch().rowsUpdated(); ---- 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") - .fetch().awaitRowsUpdated() + .bind("fn", "Joe") + .fetch().awaitRowsUpdated() ---- ====== @@ -337,9 +336,9 @@ The following example shows parameter binding for a query: [source,java] ---- - db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bind("id", "joe") - .bind("name", "Joe") + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") .bind("age", 34); ---- @@ -364,6 +363,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,26 +420,26 @@ 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}); tuples.add(new Object[] {"Ann", 50}); client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") - .bind("tuples", tuples); + .bind("tuples", tuples); ---- Kotlin:: + -[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 (name, age) IN (:tuples)") - .bind("tuples", tuples) + .bind("tuples", tuples) ---- ====== @@ -430,27 +451,27 @@ 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)); + .bind("ages", Arrays.asList(35, 50)); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") - .bind("ages", arrayOf(35, 50)) + .bind("ages", arrayOf(35, 50)) ---- ====== 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]] @@ -465,17 +486,17 @@ 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"))) - .bind("name", …) - .bind("state", …); + .filter((s, next) -> next.execute(s.returnGeneratedValues("id"))) + .bind("name", …) + .bind("state", …); ---- 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")) } @@ -491,24 +512,24 @@ 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")); + .filter(statement -> s.returnGeneratedValues("id")); client.sql("SELECT id, name, state FROM table") - .filter(statement -> s.fetchSize(25)); + .filter(statement -> s.fetchSize(25)); ---- 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") } + .filter { statement -> s.returnGeneratedValues("id") } client.sql("SELECT id, name, state FROM table") - .filter { statement -> s.fetchSize(25) } + .filter { statement -> s.fetchSize(25) } ---- ====== @@ -534,7 +555,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 { @@ -550,7 +571,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { @@ -572,7 +593,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 { @@ -593,7 +614,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> @@ -617,6 +638,7 @@ databases, you may want multiple `DatabaseClient` instances, which requires mult `ConnectionFactory` and, subsequently, multiple differently configured `DatabaseClient` instances. + [[r2dbc-auto-generated-keys]] == Retrieving Auto-generated Keys @@ -629,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")) @@ -641,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") } @@ -664,7 +686,6 @@ This section covers: * xref:data-access/r2dbc.adoc#r2dbc-TransactionAwareConnectionFactoryProxy[Using `TransactionAwareConnectionFactoryProxy`] * xref:data-access/r2dbc.adoc#r2dbc-R2dbcTransactionManager[Using `R2dbcTransactionManager`] - [[r2dbc-ConnectionFactory]] === Using `ConnectionFactory` @@ -694,20 +715,19 @@ 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"); ---- ====== - [[r2dbc-ConnectionFactoryUtils]] === Using `ConnectionFactoryUtils` @@ -718,7 +738,6 @@ and close connections (if necessary). It supports subscriber ``Context``-bound connections with, for example `R2dbcTransactionManager`. - [[r2dbc-SingleConnectionFactory]] === Using `SingleConnectionFactory` @@ -735,7 +754,6 @@ such as pipelining if your R2DBC driver permits for such use. In contrast to a pooled `ConnectionFactory`, it reuses the same connection all the time, avoiding excessive creation of physical connections. - [[r2dbc-TransactionAwareConnectionFactoryProxy]] === Using `TransactionAwareConnectionFactoryProxy` @@ -751,7 +769,6 @@ for resource management. See the {spring-framework-api}/r2dbc/connection/TransactionAwareConnectionFactoryProxy.html[`TransactionAwareConnectionFactoryProxy`] javadoc for more details. - [[r2dbc-R2dbcTransactionManager]] === Using `R2dbcTransactionManager` @@ -765,6 +782,3 @@ Application code is required to retrieve the R2DBC `Connection` through `ConnectionFactory.create()`. All framework classes (such as `DatabaseClient`) use this strategy implicitly. If not used with a transaction manager, the lookup strategy behaves exactly like `ConnectionFactory.create()` and can therefore be used in any case. - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction.adoc index e21a2cd7acfc..3c6a6aec23e4 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction.adoc @@ -32,6 +32,3 @@ The following sections describe the Spring Framework's transaction features and The chapter also includes discussions of best practices, xref:data-access/transaction/application-server-integration.adoc[application server integration], and xref:data-access/transaction/solutions-to-common-problems.adoc[solutions to common problems]. - - - 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 4e865292cdd4..8eec2397fd64 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 @@ -14,6 +14,3 @@ Spring's `JtaTransactionManager` is the standard choice to run on Jakarta EE app servers and is known to work on all common servers. Advanced functionality, such as transaction suspension, works on many servers as well (including GlassFish, JBoss and Geronimo) without any special configuration required. - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative.adoc index a87442d89167..ef690da3ef6e 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative.adoc @@ -49,5 +49,3 @@ transaction automatically on an application exception (that is, a checked except other than `java.rmi.RemoteException`). While the Spring default behavior for declarative transaction management follows EJB convention (roll back is automatic only on unchecked exceptions), it is often useful to customize this behavior. - - 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 780d4ab68581..50b6c5e7bb89 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 @@ -8,7 +8,7 @@ danger of undue coupling, because code that is meant to be used transactionally almost always deployed that way anyway. NOTE: The standard `jakarta.transaction.Transactional` annotation is also supported as -a drop-in replacement to Spring's own annotation. Please refer to the JTA documentation +a drop-in replacement for Spring's own annotation. Please refer to the JTA documentation for more details. The ease-of-use afforded by the use of the `@Transactional` annotation is best @@ -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 @@ -89,47 +89,16 @@ annotation in a `@Configuration` class. See the {spring-framework-api}/transaction/annotation/EnableTransactionManagement.html[javadoc] for full details. -In XML configuration, the `` tag provides similar convenience: +The following example shows the configuration needed to enable annotation-driven transaction management: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - <1> - - - - - +include-code::./AppConfig[tag=snippet,indent=0] - - - ----- -<1> The line that makes the bean instance transactional. - -TIP: You can omit the `transaction-manager` attribute in the `` -tag if the bean name of the `TransactionManager` that you want to wire in has the name -`transactionManager`. If the `TransactionManager` bean that you want to dependency-inject -has any other name, you have to use the `transaction-manager` attribute, as in the -preceding example. +TIP: In programmatic configuration, the `@EnableTransactionManagement` annotation uses any +`TransactionManager` bean in the context. In XML configuration, you can omit +the `transaction-manager` attribute in the `` tag if the bean +name of the `TransactionManager` that you want to wire in has the name `transactionManager`. +If the `TransactionManager` bean has any other name, you have to use the +`transaction-manager` attribute explicitly, as in the preceding example. Reactive transactional methods use reactive return types in contrast to imperative programming arrangements as the following listing shows: @@ -138,7 +107,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 @@ -168,7 +137,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 @@ -234,8 +203,10 @@ on an interface, a class definition, or a method on a class. However, the mere p of the `@Transactional` annotation is not enough to activate the transactional behavior. The `@Transactional` annotation is merely metadata that can be consumed by corresponding runtime infrastructure which uses that metadata to configure the appropriate beans with -transactional behavior. In the preceding example, the `` element -switches on actual transaction management at runtime. +transactional behavior. In the preceding examples that use programmatic configuration, +the `@EnableTransactionManagement` annotation switches on actual transaction management +at runtime. Whereas, in the preceding example that uses XML configuration, the +`` element switches on actual transaction management at runtime. TIP: The Spring team recommends that you annotate methods of concrete classes with the `@Transactional` annotation, rather than relying on annotated methods in interfaces, @@ -250,7 +221,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 @@ -327,7 +298,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 { @@ -346,7 +317,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Transactional(readOnly = true) class DefaultFooService : FooService { @@ -364,6 +335,7 @@ Kotlin:: ---- ====== + [[transaction-declarative-attransactional-settings]] == `@Transactional` Settings @@ -469,6 +441,7 @@ name of the transactionally advised class + `.` + the method name. For example, `handlePayment(..)` method of the `BusinessService` class started a transaction, the name of the transaction would be `com.example.BusinessService.handlePayment`. + [[tx-multiple-tx-mgrs-with-attransactional]] == Multiple Transaction Managers with `@Transactional` @@ -485,7 +458,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 { @@ -502,7 +475,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- class TransactionalService { @@ -564,13 +537,14 @@ 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, -e.g. with a value of "order" it can be used for autowiring purposes (identifying +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 @@ -583,7 +557,7 @@ following annotation definitions: ====== 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) @@ -600,7 +574,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) @@ -620,7 +594,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 { @@ -638,7 +612,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- class TransactionalService { @@ -658,5 +632,3 @@ Kotlin:: In the preceding example, we used the syntax to define the transaction manager qualifier and transactional labels, but we could also have included propagation behavior, rollback rules, timeouts, and other features. - - 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..59826ab473c6 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 @@ -218,5 +218,3 @@ aspect bean's `order` property so that it is higher than the transactional advic order value. You can configure additional aspects in similar fashion. - - 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 af4de5b40077..58b1847460b9 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 @@ -9,12 +9,14 @@ and then link (weave) your application with the `spring-aspects.jar` file. You must also configure the aspect with a transaction manager. You can use the Spring Framework's IoC container to take care of dependency-injecting the aspect. The simplest way to configure the transaction -management aspect is to use the `` element and specify the `mode` -attribute to `aspectj` as described in xref:data-access/transaction/declarative/annotations.adoc[Using `@Transactional`]. Because -we focus here on applications that run outside of a Spring container, we show -you how to do it programmatically. +management aspect is to use the `` element and specify the +`mode` attribute to `aspectj` as described in +xref:data-access/transaction/declarative/annotations.adoc[Using `@Transactional`]. +Because we focus here on applications that run outside of a Spring container, +we show you how to do it programmatically. -NOTE: Prior to continuing, you may want to read xref:data-access/transaction/declarative/annotations.adoc[Using `@Transactional`] and +NOTE: Prior to continuing, you may want to read +xref:data-access/transaction/declarative/annotations.adoc[Using `@Transactional`] and xref:core/aop.adoc[AOP] respectively. The following example shows how to create a transaction manager and configure the @@ -24,7 +26,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 +37,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()) @@ -58,8 +60,6 @@ regardless of visibility. To weave your applications with the `AnnotationTransactionAspect`, you must either build 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. - - - +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/diff-tx.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/diff-tx.adoc index 4881f06f5c98..7a1eb90b74d7 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/diff-tx.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/diff-tx.adoc @@ -110,5 +110,3 @@ transactional settings: ---- - - 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..633b8169c013 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 @@ -425,5 +425,3 @@ computation sequence along with a promise to begin and complete the computation. A `Publisher` can emit data while a transaction is ongoing but not necessarily completed. Therefore, methods that depend upon successful completion of an entire transaction need to ensure completion and buffer results in the calling code. - - 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 166fb732431b..42ad16cd0e9e 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 https://docs.vavr.io/#_try[official Vavr documentation]. +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() { @@ -54,7 +54,7 @@ preferring exposure in the returned handle rather than rethrowing an exception: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Transactional @Async public CompletableFuture myTransactionalMethod() { @@ -81,16 +81,18 @@ thrown, and the rules are based on exception types or exception patterns. Rollback rules may be configured in XML via the `rollback-for` and `no-rollback-for` attributes, which allow rules to be defined as patterns. When using -xref:data-access/transaction/declarative/annotations.adoc#transaction-declarative-attransactional-settings[`@Transactional`], rollback rules may -be configured via the `rollbackFor`/`noRollbackFor` and +xref:data-access/transaction/declarative/annotations.adoc#transaction-declarative-attransactional-settings[`@Transactional`], +rollback rules may be configured via the `rollbackFor`/`noRollbackFor` and `rollbackForClassName`/`noRollbackForClassName` attributes, which allow rules to be defined based on exception types or patterns, respectively. -When a rollback rule is defined with an exception type, that type will be used to match -against the type of a thrown exception and its super types, providing type safety and -avoiding any unintentional matches that may occur when using a pattern. For example, a -value of `jakarta.servlet.ServletException.class` will only match thrown exceptions of -type `jakarta.servlet.ServletException` and its subclasses. +When a rollback rule is defined with an exception type – for example, via `rollbackFor` – +that type will be used to match against the type of a thrown exception. Specifically, +given a configured exception type `C`, a thrown exception of type `T` will be considered +a match against `C` if `T` is equal to `C` or a subclass of `C`. This provides type +safety and avoids any unintentional matches that may occur when using a pattern. For +example, a value of `jakarta.servlet.ServletException.class` will only match thrown +exceptions of type `jakarta.servlet.ServletException` and its subclasses. When a rollback rule is defined with an exception pattern, the pattern can be a fully qualified class name or a substring of a fully qualified class name for an exception type @@ -172,7 +174,7 @@ rollback: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- public void resolvePosition() { try { @@ -186,7 +188,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- fun resolvePosition() { try { @@ -202,5 +204,3 @@ Kotlin:: You are strongly encouraged to use the declarative approach to rollback, if at all possible. Programmatic rollback is available should you absolutely need it, but its usage flies in the face of achieving a clean POJO-based architecture. - - 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 e27f496de9fe..eb71fd52e9d2 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 @@ -41,12 +41,10 @@ operations need to execute within the same Reactor context in the same reactive 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`, e.g. through the +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: image::tx.png[] - - 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 b41fd48f0d51..8575326837b1 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 @@ -8,6 +8,7 @@ details some of the semantics regarding transaction propagation in Spring. In Spring-managed transactions, be aware of the difference between physical and logical transactions, and how the propagation setting applies to this difference. + [[tx-propagation-required]] == Understanding `PROPAGATION_REQUIRED` @@ -21,7 +22,7 @@ where all the underlying resources have to participate in the service-level tran NOTE: By default, a participating transaction joins the characteristics of the outer scope, silently ignoring the local isolation level, timeout value, or read-only flag (if any). -Consider switching the `validateExistingTransactions` flag to `true` on your transaction +Consider switching the `validateExistingTransaction` flag to `true` on your transaction manager if you want isolation level declarations to be rejected when participating in an existing transaction with a different isolation level. This non-lenient mode also rejects read-only mismatches (that is, an inner read-write transaction that tries to participate @@ -45,6 +46,7 @@ is not aware) silently marks a transaction as rollback-only, the outer caller st calls commit. The outer caller needs to receive an `UnexpectedRollbackException` to indicate clearly that a rollback was performed instead. + [[tx-propagation-requires_new]] == Understanding `PROPAGATION_REQUIRES_NEW` @@ -67,6 +69,7 @@ for their inner transaction, with the pool not being able to hand out any such i connection anymore. Do not use `PROPAGATION_REQUIRES_NEW` unless your connection pool is appropriately sized, exceeding the number of concurrent threads by at least 1. + [[tx-propagation-nested]] == Understanding `PROPAGATION_NESTED` @@ -75,6 +78,5 @@ 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 {spring-framework-api}/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/declarative/txadvice-settings.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/txadvice-settings.adoc index 566f44d1f65a..30f0e3f789fc 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/txadvice-settings.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/txadvice-settings.adoc @@ -59,5 +59,3 @@ that are nested within `` and `` tags: | Comma-delimited list of `Exception` instances that do not trigger rollback. For example, `com.foo.MyBusinessException,ServletException`. |=== - - 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 62749a4d5842..972a6004af3e 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 { @@ -66,6 +66,3 @@ See the {spring-framework-api}/transaction/reactive/TransactionalEventPublisher.html[`TransactionalEventPublisher`] javadoc for details. ==== - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/motivation.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/motivation.adoc index 60bf567d9c41..1cc0ab1aac3b 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/motivation.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/motivation.adoc @@ -85,6 +85,3 @@ and face a hefty rework if you need that code to run within global, container-ma transactions. With the Spring Framework, only some of the bean definitions in your configuration file need to change (rather than your code). **** - - - 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..0b22fe99a229 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 { @@ -220,6 +220,7 @@ of a `TransactionTemplate`, if a class needs to use a `TransactionTemplate` with different settings (for example, a different isolation level), you need to create two distinct `TransactionTemplate` instances. + [[tx-prog-operator]] == Using the `TransactionalOperator` @@ -240,7 +241,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 +266,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 +294,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 +308,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- transactionalOperator.execute(object : TransactionCallback() { @@ -331,7 +332,6 @@ As a result it is important to consider the operators used downstream from a tra `Publisher`. In particular in the case of a `Flux` or other multi-value `Publisher`, the full output must be consumed to allow the transaction to complete. - [[tx-prog-operator-settings]] === Specifying Transaction Settings @@ -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 { @@ -382,6 +382,7 @@ Kotlin:: ---- ====== + [[transaction-programmatic-tm]] == Using the `TransactionManager` @@ -402,7 +403,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 +422,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 @@ -440,7 +441,6 @@ Kotlin:: ---- ====== - [[transaction-programmatic-rtm]] === Using the `ReactiveTransactionManager` @@ -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 @@ -492,5 +492,3 @@ Kotlin:: } ---- ====== - - diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/resources.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/resources.adoc index a8697e77094c..033178e104ac 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/resources.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/resources.adoc @@ -12,7 +12,3 @@ For more information about the Spring Framework's transaction support, see: available from https://www.infoq.com/[InfoQ] that provides a well-paced introduction to transactions in Java. It also includes side-by-side examples of how to configure and use transactions with both the Spring Framework and EJB3. - - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/solutions-to-common-problems.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/solutions-to-common-problems.adoc index 669760b534fb..f9d7503f70ef 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/solutions-to-common-problems.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/solutions-to-common-problems.adoc @@ -18,6 +18,3 @@ it) for all your transactional operations. Otherwise, the transaction infrastruc tries to perform local transactions on such resources as container `DataSource` instances. Such local transactions do not make sense, and a good application server treats them as errors. - - - 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..35f1a900a813 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"] @@ -212,7 +212,7 @@ example declares `sessionFactory` and `txManager` beans: [source,xml,indent=0,subs="verbatim,quotes"] ---- - + @@ -226,7 +226,7 @@ example declares `sessionFactory` and `txManager` beans: - + ---- @@ -238,7 +238,7 @@ transaction coordinator and possibly also its connection release mode configurat [source,xml,indent=0,subs="verbatim,quotes"] ---- - + @@ -262,7 +262,7 @@ for enforcing the same defaults: [source,xml,indent=0,subs="verbatim,quotes"] ---- - + @@ -279,6 +279,3 @@ for enforcing the same defaults: ---- - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/tx-decl-vs-prog.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/tx-decl-vs-prog.adoc index ece5dd419cf9..ec5704aabc00 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/tx-decl-vs-prog.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/tx-decl-vs-prog.adoc @@ -15,6 +15,3 @@ declarative transaction management is usually worthwhile. It keeps transaction management out of business logic and is not difficult to configure. When using the Spring Framework, rather than EJB CMT, the configuration cost of declarative transaction management is greatly reduced. - - - diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/tx-resource-synchronization.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/tx-resource-synchronization.adoc index c3eae7a6110a..a6aae73aaa15 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/tx-resource-synchronization.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/tx-resource-synchronization.adoc @@ -78,6 +78,3 @@ code must be called and passed a standard JDBC `DataSource` interface implementa that case, it is possible that this code is usable but is participating in Spring-managed transactions. You can write your new code by using the higher-level abstractions mentioned earlier. - - - diff --git a/framework-docs/modules/ROOT/pages/index.adoc b/framework-docs/modules/ROOT/pages/index.adoc index 00f046b78886..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. @@ -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 - 2024 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.adoc b/framework-docs/modules/ROOT/pages/integration.adoc index 4ab4774f7551..e6c70f547657 100644 --- a/framework-docs/modules/ROOT/pages/integration.adoc +++ b/framework-docs/modules/ROOT/pages/integration.adoc @@ -4,11 +4,3 @@ This part of the reference documentation covers Spring Framework's integration with a number of technologies. - - - - - - - - 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..57d0aed17c16 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc @@ -0,0 +1,120 @@ +[[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 +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 +latter if available in the JVM version you 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 a 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 the 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 and then exit your Spring application once the +`ApplicationContext` has refreshed: + + +-- +[tabs] +====== + +AOT cache (Java 25+):: ++ +[source,bash,subs="verbatim,quotes"] +---- +java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar application.jar ... +---- + +AOT cache (Java 24):: ++ +[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 ... +---- +====== +-- + +NOTE: With Java 25+, AOT cache stores, among other things, the +https://openjdk.org/jeps/515[method profiling information]. Therefore, to benefit of this capability, +it is recommended to create an AOT cache for an application that experienced a portion of a +production-like workflow instead of using the `-Dspring.context.exit=onRefresh` flag which designed to +optimize only the startup of your application. + +== 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 an `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 cannot 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 as the one used to create it, in the same order. +Additional JARs or directories can be specified *at the end* (but will not be cached). diff --git a/framework-docs/modules/ROOT/pages/integration/appendix.adoc b/framework-docs/modules/ROOT/pages/integration/appendix.adoc index 78f99fc50ba6..041eaa39da01 100644 --- a/framework-docs/modules/ROOT/pages/integration/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/integration/appendix.adoc @@ -1,16 +1,11 @@ [[appendix]] = Appendix - - - [[appendix.xsd-schemas]] == XML Schemas This part of the appendix lists XML schemas related to integration technologies. - - [[appendix.xsd-schemas-jee]] === The `jee` Schema @@ -172,7 +167,7 @@ different properties with `jee`: The `` element configures a reference to a local EJB Stateless Session Bean. -The following example shows how to configures a reference to a local EJB Stateless Session Bean +The following example shows how to configure a reference to a local EJB Stateless Session Bean without `jee`: [source,xml,indent=0,subs="verbatim,quotes"] @@ -184,7 +179,7 @@ without `jee`: ---- -The following example shows how to configures a reference to a local EJB Stateless Session Bean +The following example shows how to configure a reference to a local EJB Stateless Session Bean with `jee`: [source,xml,indent=0,subs="verbatim,quotes"] @@ -200,7 +195,7 @@ with `jee`: The `` element configures a reference to a local EJB Stateless Session Bean. -The following example shows how to configures a reference to a local EJB Stateless Session Bean +The following example shows how to configure a reference to a local EJB Stateless Session Bean and a number of properties without `jee`: [source,xml,indent=0,subs="verbatim,quotes"] @@ -215,7 +210,7 @@ and a number of properties without `jee`: ---- -The following example shows how to configures a reference to a local EJB Stateless Session Bean +The following example shows how to configure a reference to a local EJB Stateless Session Bean and a number of properties with `jee`: [source,xml,indent=0,subs="verbatim,quotes"] @@ -234,7 +229,7 @@ and a number of properties with `jee`: The `` element configures a reference to a `remote` EJB Stateless Session Bean. -The following example shows how to configures a reference to a remote EJB Stateless Session Bean +The following example shows how to configure a reference to a remote EJB Stateless Session Bean without `jee`: [source,xml,indent=0,subs="verbatim,quotes"] @@ -251,7 +246,7 @@ without `jee`: ---- -The following example shows how to configures a reference to a remote EJB Stateless Session Bean +The following example shows how to configure a reference to a remote EJB Stateless Session Bean with `jee`: [source,xml,indent=0,subs="verbatim,quotes"] @@ -313,7 +308,7 @@ xref:integration/jmx/naming.adoc#jmx-context-mbeanexport[Configuring Annotation- === The `cache` Schema You can use the `cache` elements to enable support for Spring's `@CacheEvict`, `@CachePut`, -and `@Caching` annotations. It it also supports declarative XML-based caching. See +and `@Caching` annotations. The `cache` schema also supports declarative XML-based caching. See xref:integration/cache/annotations.adoc#cache-annotation-enable[Enabling Caching Annotations] and xref:integration/cache/declarative-xml.adoc[Declarative XML-based Caching] for details. diff --git a/framework-docs/modules/ROOT/pages/integration/cache.adoc b/framework-docs/modules/ROOT/pages/integration/cache.adoc index 2f763a709d32..d215dcafa088 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache.adoc @@ -9,6 +9,3 @@ minimal impact on the code. In Spring Framework 4.1, the cache abstraction was significantly extended with support for xref:integration/cache/jsr-107.adoc[JSR-107 annotations] and more customization options. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc index 0398dbd90a85..2d1b2e0d29af 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc @@ -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]] @@ -648,11 +649,11 @@ triggers cache population or eviction. This is quite handy as a template mechani as it eliminates the need to duplicate cache annotation declarations, which is especially useful if the key or condition are specified or if the foreign imports (`org.springframework`) are not allowed in your code base. Similarly to the rest -of the xref:core/beans/classpath-scanning.adoc#beans-stereotype-annotations[stereotype] annotations, you can -use `@Cacheable`, `@CachePut`, `@CacheEvict`, and `@CacheConfig` as -xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] (that is, annotations that -can annotate other annotations). In the following example, we replace a common -`@Cacheable` declaration with our own custom annotation: +of the xref:core/beans/classpath-scanning.adoc#beans-stereotype-annotations[stereotype] +annotations, you can use `@Cacheable`, `@CachePut`, `@CacheEvict`, and `@CacheConfig` +as xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] +(that is, annotations that can annotate other annotations). In the following example, +we replace a common `@Cacheable` declaration with our own custom annotation: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -683,7 +684,5 @@ preceding code: Even though `@SlowService` is not a Spring annotation, the container automatically picks up its declaration at runtime and understands its meaning. Note that, as mentioned -xref:integration/cache/annotations.adoc#cache-annotation-enable[earlier], annotation-driven behavior needs to be enabled. - - - +xref:integration/cache/annotations.adoc#cache-annotation-enable[earlier], +annotation-driven behavior needs to be enabled. diff --git a/framework-docs/modules/ROOT/pages/integration/cache/declarative-xml.adoc b/framework-docs/modules/ROOT/pages/integration/cache/declarative-xml.adoc index 7b28afd74675..82bf1b932baa 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/declarative-xml.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/declarative-xml.adoc @@ -51,6 +51,3 @@ However, through XML, it is easier to apply package or group or interface-wide c (again, due to the AspectJ pointcut) and to create template-like definitions (as we did in the preceding example by defining the target cache through the `cache:definitions` `cache` attribute). - - - diff --git a/framework-docs/modules/ROOT/pages/integration/cache/jsr-107.adoc b/framework-docs/modules/ROOT/pages/integration/cache/jsr-107.adoc index 1bf06494097b..f771a379cb08 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/jsr-107.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/jsr-107.adoc @@ -118,6 +118,3 @@ NOTE: Depending on your use case, the choice is basically yours. You can even mi match services by using the JSR-107 API on some and using Spring's own annotations on others. However, if these services impact the same caches, you should use a consistent and identical key generation implementation. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/cache/plug.adoc b/framework-docs/modules/ROOT/pages/integration/cache/plug.adoc index 56e3aa482ca1..5f9f9619cd90 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/plug.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/plug.adoc @@ -10,6 +10,3 @@ caching abstraction framework on top of the storage API, as the _Caffeine_ class Most `CacheManager` classes can use the classes in the `org.springframework.cache.support` package (such as `AbstractCacheManager` which takes care of the boiler-plate code, leaving only the actual mapping to be completed). - - - diff --git a/framework-docs/modules/ROOT/pages/integration/cache/specific-config.adoc b/framework-docs/modules/ROOT/pages/integration/cache/specific-config.adoc index c05c6bda3bd7..1dce814b0295 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/specific-config.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/specific-config.adoc @@ -8,4 +8,3 @@ policies and different topologies that other solutions do not support (for examp the JDK `ConcurrentHashMap` -- exposing that in the cache abstraction would be useless because there would no backing support). Such functionality should be controlled directly through the backing cache (when configuring it) or through its native API. - 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 ed350b2385e5..37791a036d32 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/store-configuration.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/store-configuration.adoc @@ -35,7 +35,7 @@ xref:integration/cache/store-configuration.adoc#cache-store-configuration-jsr107 [[cache-store-configuration-caffeine]] == Caffeine Cache -Caffeine is a Java 8 rewrite of Guava's cache, and its implementation is located in the +Caffeine is a rewrite of Guava's cache, and its implementation is located in the `org.springframework.cache.caffeine` package and provides access to several features of Caffeine. @@ -94,6 +94,3 @@ handled by the configured cache managers. That is, every cache definition not fo either `jdkCache` or `gemfireCache` (configured earlier in the example) is handled by the no-op cache, which does not store any information, causing the target method to be invoked every time. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/cache/strategies.adoc b/framework-docs/modules/ROOT/pages/integration/cache/strategies.adoc index 4235f0617454..2b771dd238bc 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/strategies.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/strategies.adoc @@ -45,11 +45,11 @@ that is, the abstraction frees you from having to write the caching logic but do provide the actual data store. This abstraction is materialized by the `org.springframework.cache.Cache` and `org.springframework.cache.CacheManager` interfaces. -Spring provides xref:integration/cache/store-configuration.adoc[a few implementations] of that abstraction: -JDK `java.util.concurrent.ConcurrentMap` based caches, Gemfire cache, +Spring provides xref:integration/cache/store-configuration.adoc[a few implementations] +of that abstraction: JDK `java.util.concurrent.ConcurrentMap` based caches, Gemfire cache, https://github.com/ben-manes/caffeine/wiki[Caffeine], and JSR-107 compliant caches (such -as Ehcache 3.x). See xref:integration/cache/plug.adoc[Plugging-in Different Back-end Caches] for more information on plugging in other cache -stores and providers. +as Ehcache 3.x). See xref:integration/cache/plug.adoc[Plugging-in Different Back-end Caches] +for more information on plugging in other cache stores and providers. IMPORTANT: The caching abstraction has no special handling for multi-threaded and multi-process environments, as such features are handled by the cache implementation. @@ -71,6 +71,3 @@ To use the cache abstraction, you need to take care of two aspects: * Caching declaration: Identify the methods that need to be cached and their policies. * Cache configuration: The backing cache where the data is stored and from which it is read. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/cds.adoc b/framework-docs/modules/ROOT/pages/integration/cds.adoc deleted file mode 100644 index 93eb1d8afe10..000000000000 --- a/framework-docs/modules/ROOT/pages/integration/cds.adoc +++ /dev/null @@ -1,72 +0,0 @@ -[[cds]] -= CDS -:page-aliases: integration/class-data-sharing.adoc - -Class Data Sharing (CDS) is a https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[JVM feature] -that can help reduce the startup time and memory footprint of Java applications. - -To use this feature, a CDS archive should be created for the particular classpath of the -application. The Spring Framework provides a hook-point to ease the creation of the -archive. Once the archive is available, users should opt in to use it via a JVM flag. - -== Creating the CDS Archive - -A CDS archive for an application can be created when the application exits. The Spring -Framework provides a mode of operation where the process can exit automatically once the -`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons -have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been -invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet -been published. - -To create the archive, two additional JVM flags must be specified: - -* `-XX:ArchiveClassesAtExit=application.jsa`: creates the CDS archive on exit -* `-Dspring.context.exit=onRefresh`: starts and then immediately exits your Spring - application as described above - -To create a CDS archive, your JDK/JRE must have a base image. If you add the flags above to -your startup script, you may get a warning that looks like this: - -[source,shell,indent=0,subs="verbatim"] ----- - -XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded. Run with -Xlog:cds for more info. ----- - -The base CDS archive is usually provided out-of-the-box, but can also be created if needed by issuing the following -command: - -[source,shell,indent=0,subs="verbatim"] ----- - $ java -Xshare:dump ----- - -== Using the Archive - -Once the archive is available, add `-XX:SharedArchiveFile=application.jsa` to your startup -script to use it, assuming an `application.jsa` file in the working directory. - -To check if the CDS cache is effective, you can use (for testing purposes only, not in production) `-Xshare:on` which -prints an error message and exits if CDS can't be enabled. - -To figure out how effective the cache is, you can enable class loading logs by adding -an extra attribute: `-Xlog:class+load:file=cds.log`. This creates a `cds.log` with every -attempt to load a class and its source. Classes that are loaded from the cache should have -a "shared objects file" source, as shown in the following example: - -[source,shell,indent=0,subs="verbatim"] ----- - [0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top) - [0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top) - [0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top) - [0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top) - [0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top) ----- - -If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that -the following conditions are fulfilled when creating and using the archive: - - - The very same JVM must used. - - The classpath must be specified as a list of JARs, and avoid the usage of directories and `*` wildcard characters. - - The timestamps of the JARs must be preserved. - - When using the archive, the classpath must be the same than the one used to create the archive, in the same order. -Additional JARs or directories can be specified *at the end* (but won't be cached). diff --git a/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc b/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc index 137e79efeb99..19eb3b474b6f 100644 --- a/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc +++ b/framework-docs/modules/ROOT/pages/integration/checkpoint-restore.adoc @@ -13,14 +13,18 @@ WARNING: The files generated in the path specified by `-XX:CRaCCheckpointTo=PATH 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 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` JVM system property is set, a checkpoint is created automatically at diff --git a/framework-docs/modules/ROOT/pages/integration/email.adoc b/framework-docs/modules/ROOT/pages/integration/email.adoc index 46493a7de9a0..e3561069d34e 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 @@ -234,4 +232,3 @@ tasked only with creating the data that is to be rendered in the email template sending the email. It is definitely a best practice when the content of your email messages becomes even moderately complex, and, with the Spring Framework's support classes for FreeMarker, it becomes quite easy to do. - diff --git a/framework-docs/modules/ROOT/pages/integration/jms.adoc b/framework-docs/modules/ROOT/pages/integration/jms.adoc index ee207db0e088..ba4fba9b3174 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. @@ -46,19 +46,3 @@ the `ConnectionFactory` suitable for use in standalone applications. It also con implementation of Spring's `PlatformTransactionManager` for JMS (the cunningly named `JmsTransactionManager`). This allows for seamless integration of JMS as a transactional resource into Spring's transaction management mechanisms. - -[NOTE] -==== -As of Spring Framework 5, Spring's JMS package fully supports JMS 2.0 and requires the -JMS 2.0 API to be present at runtime. We recommend the use of a JMS 2.0 compatible provider. - -If you happen to use an older message broker in your system, you may try upgrading to a -JMS 2.0 compatible driver for your existing broker generation. Alternatively, you may also -try to run against a JMS 1.1 based driver, simply putting the JMS 2.0 API jar on the -classpath but only using JMS 1.1 compatible API against your driver. Spring's JMS support -adheres to JMS 1.1 conventions by default, so with corresponding configuration it does -support such a scenario. However, please consider this for transition scenarios only. -==== - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc b/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc index db67901d7cb6..0a03f2c13c97 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc @@ -26,7 +26,7 @@ behind the scenes for each annotated method, by using a `JmsListenerContainerFac Such a container is not registered against the application context but can be easily located for management purposes by using the `JmsListenerEndpointRegistry` bean. -TIP: `@JmsListener` is a repeatable annotation on Java 8, so you can associate +TIP: `@JmsListener` is a repeatable annotation, so you can associate several JMS destinations with the same method by adding additional `@JmsListener` declarations to it. @@ -42,7 +42,7 @@ 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. @@ -243,6 +243,3 @@ as the following example shows: } } ---- - - - 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 8838f449d702..d88cd8581a45 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 @@ -33,6 +33,3 @@ It uses the same underlying resource provider contract. As with EJB 2.1 MDBs, yo message listener interface supported by your JCA provider in the Spring context as well. Spring nevertheless provides explicit "`convenience`" support for JMS, because JMS is the most common endpoint API used with the JCA endpoint management contract. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc b/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc index 8ecda386a372..ced2a42c2833 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/namespace.adoc @@ -22,7 +22,6 @@ namespace elements, you need to reference the JMS schema, as the following examp ---- <1> Referencing the JMS schema. - The namespace consists of three top-level elements: ``, `` and ``. `` enables the use of xref:integration/jms/annotated.adoc[annotation-driven listener endpoints] . `` and `` diff --git a/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc b/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc index f56dae3e3fc3..acd4b2d82634 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/receiving.adoc @@ -5,10 +5,10 @@ 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 +While JMS is typically associated with asynchronous processing, you can consume messages +synchronously. The `receive(..)` methods on `JmsTemplate` and `JmsClient` provide this functionality. During a synchronous receive, the calling thread blocks until a message becomes available. This can be a dangerous operation, since the calling thread can potentially be blocked indefinitely. The `receiveTimeout` property specifies how long @@ -16,18 +16,19 @@ 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 POJO (MDP) acts as a receiver for JMS messages. The one restriction (but see -xref:integration/jms/receiving.adoc#jms-receiving-async-message-listener-adapter[Using `MessageListenerAdapter`]) on an MDP is that it must implement -the `jakarta.jms.MessageListener` interface. Note that, if your POJO receives messages -on multiple threads, it is important to ensure that your implementation is thread-safe. +xref:integration/jms/receiving.adoc#jms-receiving-async-message-listener-adapter[Using `MessageListenerAdapter`]) +on an MDP is that it must implement the `jakarta.jms.MessageListener` interface. +Note that, if your POJO receives messages on multiple threads, it is important to +ensure that your implementation is thread-safe. The following example shows a simple implementation of an MDP: @@ -154,7 +155,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 @@ -173,7 +174,7 @@ 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). diff --git a/framework-docs/modules/ROOT/pages/integration/jms/sending.adoc b/framework-docs/modules/ROOT/pages/integration/jms/sending.adoc index a5f479b0ea63..27beb6225796 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/sending.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/sending.adoc @@ -9,39 +9,7 @@ that takes no destination argument uses the default destination. The following example uses the `MessageCreator` callback to create a text message from the supplied `Session` object: -[source,java,indent=0,subs="verbatim,quotes"] ----- - import jakarta.jms.ConnectionFactory; - import jakarta.jms.JMSException; - import jakarta.jms.Message; - import jakarta.jms.Queue; - import jakarta.jms.Session; - - import org.springframework.jms.core.MessageCreator; - import org.springframework.jms.core.JmsTemplate; - - public class JmsQueueSender { - - private JmsTemplate jmsTemplate; - private Queue queue; - - public void setConnectionFactory(ConnectionFactory cf) { - this.jmsTemplate = new JmsTemplate(cf); - } - - public void setQueue(Queue queue) { - this.queue = queue; - } - - public void simpleSend() { - this.jmsTemplate.send(this.queue, new MessageCreator() { - public Message createMessage(Session session) throws JMSException { - return session.createTextMessage("hello queue world"); - } - }); - } - } ----- +include-code::./JmsQueueSender[] In the preceding example, the `JmsTemplate` is constructed by passing a reference to a `ConnectionFactory`. As an alternative, a zero-argument constructor and @@ -59,8 +27,8 @@ If you created the `JmsTemplate` and specified a default destination, the `send(MessageCreator c)` sends a message to that destination. -[[jms-msg-conversion]] -== Using Message Converters +[[jms-sending-conversion]] +== Using JMS Message Converters To facilitate the sending of domain model objects, the `JmsTemplate` has various send methods that take a Java object as an argument for a message's data @@ -84,21 +52,7 @@ gives you access to the message after it has been converted but before it is sen following example shows how to modify a message header and a property after a `java.util.Map` is converted to a message: -[source,java,indent=0,subs="verbatim,quotes"] ----- - public void sendWithConversion() { - Map map = new HashMap(); - map.put("Name", "Mark"); - map.put("Age", new Integer(47)); - jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() { - public Message postProcessMessage(Message message) throws JMSException { - message.setIntProperty("AccountID", 1234); - message.setJMSCorrelationID("123-00001"); - return message; - } - }); - } ----- +include-code::./JmsSenderWithConversion[] This results in a message of the following form: @@ -120,9 +74,14 @@ MapMessage={ } ---- +NOTE: This JMS-specific `org.springframework.jms.support.converter.MessageConverter` +arrangement operates on JMS message types and is responsible for immediate conversion +to `jakarta.jms.TextMessage`, `jakarta.jms.BytesMessage`, etc. For a contract supporting +generic message payloads, use `org.springframework.messaging.converter.MessageConverter` +with `JmsMessagingTemplate` or preferably `JmsClient` as your central delegate instead. -[[jms-callbacks]] -== Using `SessionCallback` and `ProducerCallback` +[[jms-sending-callbacks]] +== Using `SessionCallback` and `ProducerCallback` on `JmsTemplate` While the send operations cover many common usage scenarios, you might sometimes want to perform multiple operations on a JMS `Session` or `MessageProducer`. The @@ -131,4 +90,19 @@ want to perform multiple operations on a JMS `Session` or `MessageProducer`. The these callback methods. +[[jms-sending-jmsclient]] +== Sending a Message with `JmsClient` + +include-code::./JmsClientSample[] + + +[[jms-sending-postprocessor]] +== Post-processing outgoing messages + +Applications often need to intercept messages before they are sent out, for example to add message properties to all outgoing messages. +The `org.springframework.messaging.core.MessagePostProcessor` based on the spring-messaging `Message` can do that, +when configured on the `JmsClient`. It will be used for all outgoing messages sent with the `send` and `sendAndReceive` methods. + +Here is an example of an interceptor adding a "tenantId" property to all outgoing messages. +include-code::./JmsClientWithPostProcessor[] diff --git a/framework-docs/modules/ROOT/pages/integration/jms/using.adoc b/framework-docs/modules/ROOT/pages/integration/jms/using.adoc index 027098cbc205..5e3247ef3327 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/using.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/using.adoc @@ -4,13 +4,20 @@ This section describes how to use Spring's JMS components. -[[jms-jmstemplate]] -== Using `JmsTemplate` +[[jms-jmstemplate-jmsclient]] +== `JmsTemplate` and `JmsClient` The `JmsTemplate` class is the central class in the JMS core package. It simplifies the use of JMS, since it handles the creation and release of resources when sending or synchronously receiving messages. +`JmsClient` is a new API variant in Spring Framework 7.0, following the design of +`JdbcClient` and co. `JmsClient` builds on `JmsTemplate` for straightforward send +and receive operations with customization options per operation. + +[[jms-jmstemplate]] +=== Using `JmsTemplate` + Code that uses the `JmsTemplate` needs only to implement callback interfaces that give them a clearly defined high-level contract. The `MessageCreator` callback interface creates a message when given a `Session` provided by the calling code in `JmsTemplate`. To @@ -43,10 +50,23 @@ and then safely inject this shared reference into multiple collaborators. To be clear, the `JmsTemplate` is stateful, in that it maintains a reference to a `ConnectionFactory`, but this state is not conversational state. +[[jms-jmsclient]] +=== Using `JmsClient` + As of Spring Framework 4.1, `JmsMessagingTemplate` is built on top of `JmsTemplate` -and provides an integration with the messaging abstraction -- that is, -`org.springframework.messaging.Message`. This lets you create the message to -send in a generic manner. +and provides an integration with the Spring's common messaging abstraction -- that is, +handling `org.springframework.messaging.Message` for sending and receiving, +throwing `org.springframework.messaging.MessagingException` and with payload conversion +going through `org.springframework.messaging.converter.MessageConverter` (with many +common converter implementations available). + +As of Spring Framework 7.0, a fluent API called `JmsClient` is available. This provides +customizable operations around `org.springframework.messaging.Message` and throwing +`org.springframework.messaging.MessagingException`, similar to `JmsMessagingTemplate`, +as well as integration with `org.springframework.messaging.converter.MessageConverter`. +A `JmsClient can either be created for a given `ConnectionFactory` or for a given +`JmsTemplate`, in the latter case reusing its settings by default. See +{spring-framework-api}/jms/core/JmsClient.html[`JmsClient`] for usage examples. [[jms-connections]] @@ -167,13 +187,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,7 +249,7 @@ 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. @@ -246,7 +268,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. @@ -291,6 +313,3 @@ in an unmanaged environment, you can specify these values through the use of the properties `sessionTransacted` and `sessionAcknowledgeMode`. When you use a `PlatformTransactionManager` with `JmsTemplate`, the template is always given a transactional JMS `Session`. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jmx.adoc b/framework-docs/modules/ROOT/pages/integration/jmx.adoc index 40e2bcd0096b..ebc04a0c19be 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx.adoc @@ -21,6 +21,3 @@ These features are designed to work without coupling your application components either Spring or JMX interfaces and classes. Indeed, for the most part, your application classes need not be aware of either Spring or JMX in order to take advantage of the Spring JMX features. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/exporting.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/exporting.adoc index 138c867a20c3..4a32631fb647 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/exporting.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/exporting.adoc @@ -231,6 +231,3 @@ behavior to the `REPLACE_EXISTING` behavior: ---- - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/interface.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/interface.adoc index f9458765aac1..82391e2c4784 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. + +| `@ManagedMetric` +| Methods (only getters) +| Marks a getter as a JMX attribute, with added descriptor properties to indicate that it is a metric. -| Define descriptions for operation parameters. -| `@ManagedOperationParameter` and `@ManagedOperationParameters` -| Method +| `@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]] @@ -412,6 +425,3 @@ appropriate half of a JMX attribute. In the preceding code, the method mappings beans that are exposed to JMX. To control method exposure on a bean-by-bean basis, you can use the `methodMappings` property of `MethodNameMBeanInfoAssembler` to map bean names to lists of method names. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/jsr160.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/jsr160.adoc index 7d9ce296d2b1..90a1a8e3ab90 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/jsr160.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/jsr160.adoc @@ -98,6 +98,3 @@ as the following example shows: In the preceding example, we used MX4J 3.0.0. See the official MX4J documentation for more information. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/naming.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/naming.adoc index cdaf80432237..1d5374183a0e 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/naming.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/naming.adoc @@ -141,6 +141,3 @@ also hides the JMX-managed resource annotations. Hence, you should use target-cl case (through setting the 'proxy-target-class' flag on ``, `` and so on). Otherwise, your JMX beans might be silently ignored at startup. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc index 4811434a9c70..5d5e4ae90341 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/notifications.adoc @@ -32,7 +32,7 @@ example writes notifications to the console: } public boolean isNotificationEnabled(Notification notification) { - return AttributeChangeNotification.class.isAssignableFrom(notification.getClass()); + return (notification instanceof AttributeChangeNotification); } } @@ -303,6 +303,3 @@ the nicer features of Spring's JMX support. It does, however, come with the pric coupling your classes to both Spring and JMX. As always, the advice here is to be pragmatic. If you need the functionality offered by the `NotificationPublisher` and you can accept the coupling to both Spring and JMX, then do so. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/proxy.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/proxy.adoc index 317d9409689c..ecaedbedaaa3 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/proxy.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/proxy.adoc @@ -44,6 +44,3 @@ that uses the `MBeanServerConnectionFactoryBean`. This `MBeanServerConnection` i passed to the `MBeanProxyFactoryBean` through the `server` property. The proxy that is created forwards all invocations to the `MBeanServer` through this `MBeanServerConnection`. - - - diff --git a/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc b/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc index 7e6164bc86e6..4cda87043e86 100644 --- a/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jmx/resources.adoc @@ -10,4 +10,3 @@ homepage] at Oracle. * 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 e4208a28d8f8..e2c641a19bd6 100644 --- a/framework-docs/modules/ROOT/pages/integration/observability.adoc +++ b/framework-docs/modules/ROOT/pages/integration/observability.adoc @@ -1,13 +1,18 @@ [[observability]] = Observability Support -Micrometer defines an https://micrometer.io/docs/observation[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. +With https://docs.micrometer.io/micrometer/reference/concepts.html[Micrometer], developers can instrument libraries and applications for metrics (timers, gauges, counters) +that collect statistics about their runtime behavior. Metrics can help you to track error rates, usage patterns, performance, and more. +https://docs.micrometer.io/tracing/reference/[Micrometer can also produce traces], giving you 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. + +Micrometer defines an {micrometer-docs}/observation.html[Observation concept that enables both Metrics and Traces] in applications. +Each observation will produce: + +* https://docs.micrometer.io/micrometer/reference/observation/components.html#micrometer-observation-default-meter-handler[several metrics - a timer, a long task timer and many counters] +* a https://docs.micrometer.io/tracing/reference/glossary.html[span for the current trace] Spring Framework instruments various parts of its own codebase to publish observations if an `ObservationRegistry` is configured. -You can learn more about {spring-boot-docs}/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 +42,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...). @@ -88,6 +93,7 @@ include-code::./ServerRequestObservationFilter[] You can configure `ObservationFilter` instances on the `ObservationRegistry`. + [[observability.tasks-scheduled]] == @Scheduled tasks instrumentation @@ -97,7 +103,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,7 +113,7 @@ 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.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. @@ -126,7 +132,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`: @@ -138,7 +144,7 @@ By default, both observations share the same set of possible `KeyValues`: |`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 @@ -146,7 +152,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. |=== @@ -162,6 +168,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 @@ -182,16 +190,17 @@ Such listeners are set on a `MessageConsumer` within a session callback (see `Jm This observation uses the `io.micrometer.jakarta9.instrument.jms.DefaultJmsProcessObservationConvention` by default, backed by the `io.micrometer.jakarta9.instrument.jms.JmsProcessObservationContext`. + [[observability.http-server]] == HTTP Server instrumentation -HTTP server exchange observations are created with the name `"http.server.requests"` for Servlet and Reactive applications. +HTTP server exchange observations are created with the name `"http.server.requests"` for Servlet and Reactive applications, +or `"http.server.request.duration"` if using the OpenTelemetry convention. [[observability.http-server.servlet]] === Servlet applications Applications need to configure the `org.springframework.web.filter.ServerHttpObservationFilter` Servlet filter in their application. -It uses the `org.springframework.http.server.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 the web framework and has bubbled up to the Servlet filter. Typically, all exceptions handled by Spring MVC's `@ExceptionHandler` and xref:web/webmvc/mvc-ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation. @@ -203,6 +212,11 @@ NOTE: Because the instrumentation is done at the Servlet Filter level, the obser Typically, Servlet container error handling is performed at a lower level and won't have any active observation or span. For this use case, a container-specific implementation is required, such as a `org.apache.catalina.Valve` for Tomcat; this is outside the scope of this project. +[[observability.http-server.servlet.default]] +==== Default Semantic Convention + +It uses the `org.springframework.http.server.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`. + By default, the following `KeyValues` are created: .Low cardinality Keys @@ -211,7 +225,7 @@ By default, the following `KeyValues` are created: |Name | Description |`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 HTTP request method or `"none"` if not a well-known method. +|`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. @@ -225,6 +239,15 @@ By default, the following `KeyValues` are created: |=== +[[observability.http-server.servlet.otel]] +==== OpenTelemetry Semantic Convention + +An OpenTelemetry variant is available with `org.springframework.http.server.observation.OpenTelemetryServerRequestObservationConvention`, backed by the `ServerRequestObservationContext`. + +This variant complies with the https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/http/http-metrics.md[OpenTelemetry Semantic Conventions for HTTP Metrics (v1.36.0)] +and the https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/http/http-spans.md[OpenTelemetry Semantic Conventions for HTTP Spans (v1.36.0)]. + + [[observability.http-server.reactive]] === Reactive applications @@ -233,10 +256,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[] @@ -249,7 +272,7 @@ By default, the following `KeyValues` are created: |Name | Description |`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 HTTP request method or `"none"` if not a well-known method. +|`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. @@ -263,11 +286,11 @@ By default, the following `KeyValues` are created: |=== - [[observability.http-client]] == 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]] @@ -282,8 +305,8 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ [cols="a,a"] |=== |Name | Description -|`method` _(required)_|Name of 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. 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. @@ -298,7 +321,6 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ |`http.url` _(required)_|HTTP request URI. |=== - [[observability.http-client.restclient]] === RestClient @@ -310,8 +332,8 @@ 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. @@ -326,11 +348,10 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ |`http.url` _(required)_|HTTP request URI. |=== - [[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`. @@ -339,8 +360,8 @@ 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 not a well-known method. -|`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. @@ -366,7 +387,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 37d85683e684..4813980bbf4d 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -3,96 +3,110 @@ The Spring Framework provides the following choices for making calls to REST endpoints: -* xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] - synchronous client with a fluent API. -* xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] - non-blocking, reactive client with fluent API. -* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] - synchronous client with template method API. -* xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] - annotated interface with generated, dynamic proxy implementation. +* xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] -- synchronous client with a fluent API +* xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] -- non-blocking, reactive client with fluent API +* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] -- synchronous client with template method API, now deprecated in favor of `RestClient` +* xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service Clients] -- annotated interface backed by generated proxy [[rest-restclient]] == `RestClient` -The `RestClient` is a synchronous HTTP client that offers a modern, fluent API. -It offers an abstraction over HTTP libraries that allows for convenient conversion from a Java object to an HTTP request, and the creation of objects from an HTTP response. +`RestClient` is a synchronous HTTP client that provides a fluent API to perform requests. +It serves as an abstraction over HTTP libraries, and handles conversion of HTTP request and response content to and from higher level Java objects. -=== Creating a `RestClient` +=== Create a `RestClient` -The `RestClient` is created using one of the static `create` methods. -You can also use `builder()` to get a builder with further options, such as specifying which HTTP library to use (see <>) and which message converters to use (see <>), setting a default URI, default path variables, default request headers, or `uriBuilderFactory`, or registering interceptors and initializers. +`RestClient` has static `create` shortcut methods. +It also exposes a `builder()` with further options: -Once created (or built), the `RestClient` can be used safely by multiple threads. +- select the HTTP library to use, see <> +- configure message converters, see <> +- set a baseUrl +- set default request headers, cookies, path variables, API version +- configure an `ApiVersionInserter` +- register interceptors +- register request initializers -The following sample shows how to create a default `RestClient`, and how to build a custom one. +Once created, a `RestClient` is safe to use in multiple threads. + +The below shows how to create or build a `RestClient`: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- -RestClient defaultClient = RestClient.create(); - -RestClient customClient = RestClient.builder() - .requestFactory(new HttpComponentsClientHttpRequestFactory()) - .messageConverters(converters -> converters.add(new MyCustomMessageConverter())) - .baseUrl("https://example.com") - .defaultUriVariables(Map.of("variable", "foo")) - .defaultHeader("My-Header", "Foo") - .requestInterceptor(myCustomInterceptor) - .requestInitializer(myCustomInitializer) - .build(); + RestClient defaultClient = RestClient.create(); + + RestClient customClient = RestClient.builder() + .requestFactory(new HttpComponentsClientHttpRequestFactory()) + .messageConverters(converters -> converters.add(new MyCustomMessageConverter())) + .baseUrl("https://example.com") + .defaultUriVariables(Map.of("variable", "foo")) + .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") + .defaultVersion("1.2") + .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) + .requestInterceptor(myCustomInterceptor) + .requestInitializer(myCustomInitializer) + .build(); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- -val defaultClient = RestClient.create() - -val customClient = RestClient.builder() - .requestFactory(HttpComponentsClientHttpRequestFactory()) - .messageConverters { converters -> converters.add(MyCustomMessageConverter()) } - .baseUrl("https://example.com") - .defaultUriVariables(mapOf("variable" to "foo")) - .defaultHeader("My-Header", "Foo") - .requestInterceptor(myCustomInterceptor) - .requestInitializer(myCustomInitializer) - .build() + val defaultClient = RestClient.create() + + val customClient = RestClient.builder() + .requestFactory(HttpComponentsClientHttpRequestFactory()) + .messageConverters { converters -> converters.add(MyCustomMessageConverter()) } + .baseUrl("https://example.com") + .defaultUriVariables(mapOf("variable" to "foo")) + .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") + .defaultVersion("1.2") + .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) + .requestInterceptor(myCustomInterceptor) + .requestInitializer(myCustomInitializer) + .build() ---- ====== -=== Using the `RestClient` +=== Use the `RestClient` -When making an HTTP request with the `RestClient`, the first thing to specify is which HTTP method to use. -This can be done with `method(HttpMethod)` or with the convenience methods `get()`, `head()`, `post()`, and so on. +To perform an HTTP request, first specify the HTTP method to use. +Use the convenience methods like `get()`, `head()`, `post()`, and others, or `method(HttpMethod)`. ==== Request URL -Next, the request URI can be specified with the `uri` methods. -This step is optional and can be skipped if the `RestClient` is configured with a default URI. +Next, specify the request URI with the `uri` methods. +This is optional, and you can skip this step if you configured a baseUrl through the builder. The URL is typically specified as a `String`, with optional URI template variables. -The following example configures a GET request to `https://example.com/orders/42`: +The following shows how to perform a request: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- -int id = 42; -restClient.get() - .uri("https://example.com/orders/{id}", id) - .... + int id = 42; + restClient.get() + .uri("https://example.com/orders/{id}", id) + // ... ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val id = 42 -restClient.get() - .uri("https://example.com/orders/{id}", id) - ... + val id = 42 + restClient.get() + .uri("https://example.com/orders/{id}", id) + // ... ---- ====== @@ -106,6 +120,7 @@ For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri- If necessary, the HTTP request can be manipulated by adding request headers with `header(String, String)`, `headers(Consumer`, or with the convenience methods `accept(MediaType...)`, `acceptCharset(Charset...)` and so on. For HTTP requests that can contain a body (`POST`, `PUT`, and `PATCH`), additional methods are available: `contentType(MediaType)`, and `contentLength(long)`. +You can set an API version for the request if the client is configured with `ApiVersionInserter`. The request body itself can be set by `body(Object)`, which internally uses <>. Alternatively, the request body can be set using a `ParameterizedTypeReference`, allowing you to use generics. @@ -113,11 +128,15 @@ Finally, the body can be set to a callback function that writes to an `OutputStr ==== 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. +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 <>). -The response can also be converted into a `ResponseEntity`, giving access to the response headers as well as the body. +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. @@ -125,14 +144,14 @@ This sample shows how `RestClient` can be used to perform a simple `GET` request ====== 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> - .retrieve() <3> - .body(String.class); <4> - -System.out.println(result); <5> + String result = restClient.get() <1> + .uri("https://example.com") <2> + .retrieve() <3> + .body(String.class); <4> + + System.out.println(result); <5> ---- <1> Set up a GET request <2> Specify the URL to connect to @@ -142,14 +161,14 @@ 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> - .retrieve() <3> - .body() <4> - -println(result) <5> + val result= restClient.get() <1> + .uri("https://example.com") <2> + .retrieve() <3> + .body() <4> + + println(result) <5> ---- <1> Set up a GET request <2> Specify the URL to connect to @@ -164,16 +183,16 @@ 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> - .retrieve() - .toEntity(String.class); <2> - -System.out.println("Response status: " + result.getStatusCode()); <3> -System.out.println("Response headers: " + result.getHeaders()); <3> -System.out.println("Contents: " + result.getBody()); <3> + ResponseEntity result = restClient.get() <1> + .uri("https://example.com") <1> + .retrieve() + .toEntity(String.class); <2> + + System.out.println("Response status: " + result.getStatusCode()); <3> + System.out.println("Response headers: " + result.getHeaders()); <3> + System.out.println("Contents: " + result.getBody()); <3> ---- <1> Set up a GET request for the specified URL <2> Convert the response into a `ResponseEntity` @@ -181,16 +200,16 @@ 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> - .retrieve() - .toEntity() <2> - -println("Response status: " + result.statusCode) <3> -println("Response headers: " + result.headers) <3> -println("Contents: " + result.body) <3> + val result = restClient.get() <1> + .uri("https://example.com") <1> + .retrieve() + .toEntity() <2> + + println("Response status: " + result.statusCode) <3> + println("Response headers: " + result.headers) <3> + println("Contents: " + result.body) <3> ---- <1> Set up a GET request for the specified URL <2> Convert the response into a `ResponseEntity` @@ -204,14 +223,14 @@ Note the usage of URI variables in this sample and that the `Accept` header is s ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- -int id = ...; -Pet pet = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) <1> - .accept(APPLICATION_JSON) <2> - .retrieve() - .body(Pet.class); <3> + int id = ...; + Pet pet = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) <1> + .accept(APPLICATION_JSON) <2> + .retrieve() + .body(Pet.class); <3> ---- <1> Using URI variables <2> Set the `Accept` header to `application/json` @@ -219,14 +238,14 @@ 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() - .uri("https://petclinic.example.com/pets/{id}", id) <1> - .accept(APPLICATION_JSON) <2> - .retrieve() - .body() <3> + val id = ... + val pet = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) <1> + .accept(APPLICATION_JSON) <2> + .retrieve() + .body() <3> ---- <1> Using URI variables <2> Set the `Accept` header to `application/json` @@ -239,15 +258,15 @@ 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> - .uri("https://petclinic.example.com/pets/new") <2> - .contentType(APPLICATION_JSON) <3> - .body(pet) <4> - .retrieve() - .toBodilessEntity(); <5> + Pet pet = ... <1> + ResponseEntity response = restClient.post() <2> + .uri("https://petclinic.example.com/pets/new") <2> + .contentType(APPLICATION_JSON) <3> + .body(pet) <4> + .retrieve() + .toBodilessEntity(); <5> ---- <1> Create a `Pet` domain object <2> Set up a POST request, and the URL to connect to @@ -257,15 +276,15 @@ 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> - .uri("https://petclinic.example.com/pets/new") <2> - .contentType(APPLICATION_JSON) <3> - .body(pet) <4> - .retrieve() - .toBodilessEntity() <5> + val pet: Pet = ... <1> + val response = restClient.post() <2> + .uri("https://petclinic.example.com/pets/new") <2> + .contentType(APPLICATION_JSON) <3> + .body(pet) <4> + .retrieve() + .toBodilessEntity() <5> ---- <1> Create a `Pet` domain object <2> Set up a POST request, and the URL to connect to @@ -283,15 +302,15 @@ This behavior can be overridden using `onStatus`. ====== 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> - }) - .body(String.class); + String result = restClient.get() <1> + .uri("https://example.com/this-url-does-not-exist") <1> + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2> + throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3> + }) + .body(String.class); ---- <1> Create a GET request for a URL that returns a 404 status code <2> Set up a status handler for all 4xx status codes @@ -299,14 +318,14 @@ 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> - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2> - throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3> - .body() + val result = restClient.get() <1> + .uri("https://example.com/this-url-does-not-exist") <1> + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2> + throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3> + .body() ---- <1> Create a GET request for a URL that returns a 404 status code <2> Set up a status handler for all 4xx status codes @@ -322,20 +341,20 @@ Status handlers are not applied when use `exchange()`, because the exchange func ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- -Pet result = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) - .accept(APPLICATION_JSON) - .exchange((request, response) -> { <1> - if (response.getStatusCode().is4xxClientError()) { <2> - throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2> - } - else { - Pet pet = convertResponse(response); <3> - return pet; - } - }); +[source,java,indent=0,subs="verbatim,quotes"] +---- + Pet result = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) + .accept(APPLICATION_JSON) + .exchange((request, response) -> { <1> + if (response.getStatusCode().is4xxClientError()) { <2> + throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2> + } + else { + Pet pet = convertResponse(response); <3> + return pet; + } + }); ---- <1> `exchange` provides the request and response <2> Throw an exception when the response has a 4xx status code @@ -343,91 +362,29 @@ Pet result = restClient.get() Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- -val result = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) - .accept(MediaType.APPLICATION_JSON) - .exchange { request, response -> <1> - if (response.getStatusCode().is4xxClientError()) { <2> - throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2> - } else { - val pet: Pet = convertResponse(response) <3> - pet - } - } +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val result = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) + .accept(MediaType.APPLICATION_JSON) + .exchange { request, response -> <1> + if (response.getStatusCode().is4xxClientError()) { <2> + throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2> + } else { + val pet: Pet = convertResponse(response) <3> + pet + } + } ---- <1> `exchange` provides the request and response <2> Throw an exception when the response has a 4xx status code <3> Convert the response into a Pet domain object ====== - [[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 {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`. - -| `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`. - -| `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 the `messageConverters()` method 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 @@ -435,17 +392,37 @@ To serialize only a subset of the object properties, you can specify a {baeldung [source,java,indent=0,subs="verbatim"] ---- -MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); -value.setSerializationView(User.WithoutPasswordView.class); + MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); + value.setSerializationView(User.WithoutPasswordView.class); + + ResponseEntity response = restClient.post() // or RestTemplate.postForEntity + .contentType(APPLICATION_JSON) + .body(value) + .retrieve() + .toBodilessEntity(); +---- -ResponseEntity response = restClient.post() // or RestTemplate.postForEntity - .contentType(APPLICATION_JSON) - .body(value) - .retrieve() - .toBodilessEntity(); +==== URL encoded Forms +URL encoded forms, using the `"application/x-www-form-urlencoded"` media type, are useful for sending String key/values over the wire. +This is supported by the `FormHttpMessageConverter`, if the application uses a `MultiValueMap` as source instance +or a target type. + +For example: + +[source,java,indent=0,subs="verbatim"] +---- + MultiValueMap form = new LinkedMultiValueMap<>(); + form.add("project", "Spring Framework"); + form.add("module", "spring-web"); + ResponseEntity response = this.restClient.post() + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(form) + .retrieve() + .toBodilessEntity(); ---- + ==== Multipart To send multipart data, you need to provide a `MultiValueMap` whose values may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for part content with headers. @@ -453,28 +430,80 @@ For example: [source,java,indent=0,subs="verbatim"] ---- -MultiValueMap parts = new LinkedMultiValueMap<>(); - -parts.add("fieldPart", "fieldValue"); -parts.add("filePart", new FileSystemResource("...logo.png")); -parts.add("jsonPart", new Person("Jason")); - -HttpHeaders headers = new HttpHeaders(); -headers.setContentType(MediaType.APPLICATION_XML); -parts.add("xmlPart", new HttpEntity<>(myBean, headers)); - -// send using RestClient.post or RestTemplate.postForEntity + MultiValueMap parts = new LinkedMultiValueMap<>(); + + parts.add("fieldPart", "fieldValue"); + parts.add("filePart", new FileSystemResource("...logo.png")); + parts.add("jsonPart", new Person("Jason")); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + parts.add("xmlPart", new HttpEntity<>(myBean, headers)); + + ResponseEntity response = this.restClient.post() + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(parts) + .retrieve() + .toBodilessEntity(); ---- In most cases, you do not have to specify the `Content-Type` for each part. The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension. If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper. -Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`). +The `Content-Type` is set to `multipart/form-data` by the `MultipartHttpMessageConverter`. +As seen in the previous section, `MultiValueMap` types can also be used for URL encoded forms. +It is preferable to explicitly set the media type in the `Content-Type` or `Accept` HTTP request headers to ensure that the expected +message converter is used. + +`RestClient` can also receive multipart responses. +To decode a multipart response body, use a `ParameterizedTypeReference>`. +The decoded map contains `Part` instances where `FormFieldPart` represents form field values +and `FilePart` represents file parts with a `filename()` and a `transferTo()` method. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim"] +---- + MultiValueMap result = this.restClient.get() + .uri("https://example.com/upload") + .accept(MediaType.MULTIPART_FORM_DATA) + .retrieve() + .body(new ParameterizedTypeReference<>() {}); + + Part field = result.getFirst("fieldPart"); + if (field instanceof FormFieldPart formField) { + String fieldValue = formField.value(); + } + Part file = result.getFirst("filePart"); + if (file instanceof FilePart filePart) { + filePart.transferTo(Path.of("/tmp/" + filePart.filename())); + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim"] +---- + val result = this.restClient.get() + .uri("https://example.com/upload") + .accept(MediaType.MULTIPART_FORM_DATA) + .retrieve() + .body(object : ParameterizedTypeReference>() {}) + + val field = result?.getFirst("fieldPart") + if (field is FormFieldPart) { + val fieldValue = field.value() + } + val file = result?.getFirst("filePart") + if (file is FilePart) { + file.transferTo(Path.of("/tmp/" + file.filename())) + } +---- +====== -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 necessary the `Content-Type` may also be set explicitly. [[rest-request-factories]] === Client Request Factories @@ -494,9 +523,10 @@ If no request factory is specified when the `RestClient` was built, it will use 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 (e.g. 401). +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` @@ -509,22 +539,21 @@ synchronous, asynchronous, and streaming scenarios. * 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 +* Functional-style, fluent API that takes advantage of lambda expressions * Synchronous and asynchronous interactions * Streaming up to or streaming down from a server See xref:web/webflux-webclient.adoc[WebClient] for more details. - - [[rest-resttemplate]] == `RestTemplate` The `RestTemplate` provides a high-level API over HTTP client libraries in the form of a classic Spring Template class. It exposes the following groups of overloaded methods: -NOTE: The xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] offers a more modern API for synchronous HTTP access. +WARNING: As of Spring Framework 7.0, `RestTemplate` is deprecated in favor of `RestClient` and will be removed in a future version, +please use the xref:integration/rest-clients.adoc#migrating-to-restclient["Migrating to RestClient"] guide. For asynchronous and streaming scenarios, consider the reactive xref:web/webflux-webclient.adoc[WebClient]. [[rest-overview-of-resttemplate-methods-tbl]] @@ -589,12 +618,29 @@ See the xref:integration/observability.adoc#http-client.resttemplate[RestTemplat [[rest-template-body]] === Body -Objects passed into and returned from `RestTemplate` methods are converted to and from HTTP messages with the help of an `HttpMessageConverter`, see <>. +Objects passed into and returned from `RestTemplate` methods are converted to and from HTTP messages +with the help of an `HttpMessageConverter`, see <>. + +[[migrating-to-restclient]] +=== Migrating to `RestClient` + +Applications can adopt `RestClient` in a gradual fashion, first focusing on API usage and then on infrastructure setup. +You can consider the following steps: + +1. Create one or more `RestClient` from existing `RestTemplate` instances, like: `RestClient restClient = RestClient.create(restTemplate)`. + Gradually replace `RestTemplate` usage in your application, component by component, by focusing first on issuing requests. + See the table below for API equivalents. +2. Once all client requests go through `RestClient` instances, you can now work on replicating your existing + `RestTemplate` instance creations by using `RestClient.Builder`. Because `RestTemplate` and `RestClient` + share the same infrastructure, you can reuse custom `ClientHttpRequestFactory` or `ClientHttpRequestInterceptor` + in your setup. See xref:integration/rest-clients.adoc#rest-restclient[the `RestClient` builder API]. + +If no other library is available on the classpath, `RestClient` will choose the `JdkClientHttpRequestFactory` +powered by the modern JDK `HttpClient`, whereas `RestTemplate` would pick the `SimpleClientHttpRequestFactory` that +uses `HttpURLConnection`. This can explain subtle behavior difference at runtime at the HTTP level. -=== Migrating from `RestTemplate` to `RestClient` The following table shows `RestClient` equivalents for `RestTemplate` methods. -It can be used to migrate from the latter to the former. .RestClient equivalents for RestTemplate methods [cols="1,1", options="header"] @@ -898,21 +944,28 @@ It can be used to migrate from the latter to the former. |=== +`RestClient` and `RestTemplate` instances share the same behavior when it comes to throwing exceptions +(with the `RestClientException` type being at the top of the hierarchy). +When `RestTemplate` consistently throws `HttpClientErrorException` for "4xx" response statues, +`RestClient` allows for more flexibility with custom xref:integration/rest-clients.adoc#rest-http-service-client-exceptions["status handlers"]. + -[[rest-http-interface]] -== HTTP Interface +[[rest-http-service-client]] +== HTTP Service Clients -The Spring Framework lets you define an HTTP service as a Java interface with -`@HttpExchange` methods. You can pass such an interface to `HttpServiceProxyFactory` -to create a proxy which performs requests through an HTTP client such as `RestClient` -or `WebClient`. You can also implement the interface from an `@Controller` for server -request handling. +You can define an HTTP Service as a Java interface with `@HttpExchange` methods, and use +`HttpServiceProxyFactory` to create a client proxy from it for remote access over HTTP via +`RestClient`, `WebClient`, or `RestTemplate`. On the server side, an `@Controller` class +can implement the same interface to handle requests with +xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-httpexchange-annotation[@HttpExchange] +controller methods. -Start by creating the interface with `@HttpExchange` methods: + +First, create the Java interface: [source,java,indent=0,subs="verbatim,quotes"] ---- - interface RepositoryService { + public interface RepositoryService { @GetExchange("/repos/{owner}/{repo}") Repository getRepository(@PathVariable String owner, @PathVariable String repo); @@ -922,69 +975,69 @@ Start by creating the interface with `@HttpExchange` methods: } ---- -Now you can create a proxy that performs requests when methods are called. - -For `RestClient`: +Optionally, use `@HttpExchange` at the type level to declare common attributes for all methods: [source,java,indent=0,subs="verbatim,quotes"] ---- - RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build(); - RestClientAdapter adapter = RestClientAdapter.create(restClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); + @HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json") + public interface RepositoryService { - RepositoryService service = factory.createClient(RepositoryService.class); ----- + @GetExchange + Repository getRepository(@PathVariable String owner, @PathVariable String repo); -For `WebClient`: + @PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + void updateRepository(@PathVariable String owner, @PathVariable String repo, + @RequestParam String name, @RequestParam String description, @RequestParam String homepage); -[source,java,indent=0,subs="verbatim,quotes"] + } ---- - WebClient webClient = WebClient.builder().baseUrl("https://api.github.com/").build(); - WebClientAdapter adapter = WebClientAdapter.create(webClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); - RepositoryService service = factory.createClient(RepositoryService.class); ----- -For `RestTemplate`: +Next, configure the client and create the `HttpServiceProxyFactory`: [source,java,indent=0,subs="verbatim,quotes"] ---- + // Using RestClient... + + RestClient restClient = RestClient.create("..."); + RestClientAdapter adapter = RestClientAdapter.create(restClient); + + // or WebClient... + + WebClient webClient = WebClient.create("..."); + WebClientAdapter adapter = WebClientAdapter.create(webClient); + + // or RestTemplate... + RestTemplate restTemplate = new RestTemplate(); - restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/")); RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); - RepositoryService service = factory.createClient(RepositoryService.class); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); ---- -`@HttpExchange` is supported at the type level where it applies to all methods: +Now, you're ready to create client proxies: [source,java,indent=0,subs="verbatim,quotes"] ---- - @HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json") - interface RepositoryService { - - @GetExchange - Repository getRepository(@PathVariable String owner, @PathVariable String repo); - - @PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - void updateRepository(@PathVariable String owner, @PathVariable String repo, - @RequestParam String name, @RequestParam String description, @RequestParam String homepage); - - } + RepositoryService service = factory.createClient(RepositoryService.class); + // Use service methods for remote calls... ---- +HTTP service clients is a powerful and expressive choice for remote access over HTTP. +It allows one team to own the knowledge of how a REST API works, what parts are relevant +to a client application, what input and output types to create, what endpoint method +signatures are needed, what Javadoc to have, and so on. The resulting Java API guides and +is ready to use. + -[[rest-http-interface-method-parameters]] +[[rest-http-service-client-method-parameters]] === Method Parameters -Annotated, HTTP exchange methods support flexible method signatures with the following -method parameters: +`@HttpExchange` methods support flexible method signatures with the following inputs: [cols="1,2", options="header"] |=== -| Method argument | Description +| Method parameter | Description | `URI` | Dynamically set the URL for the request, overriding the annotation's `url` attribute. @@ -997,9 +1050,10 @@ 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 @@ -1007,7 +1061,8 @@ method parameters: is supported for non-String values. | `@RequestAttribute` -| Provide an `Object` to add as a request attribute. Only supported by `WebClient`. +| 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 @@ -1025,7 +1080,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` @@ -1039,8 +1094,36 @@ 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`]. + +`RestClientAdapter` provides additional support for a method parameter of type +`StreamingHttpOutputMessage.Body` that allows sending the request body by writing to an +`OutputStream`. + +[[rest-http-service-client.custom-resolver]] +=== Custom Arguments + +You can configure a custom `HttpServiceArgumentResolver`. The example interface below +uses a custom `Search` method parameter type: + +include-code::./CustomHttpServiceArgumentResolver[tag=httpserviceclient,indent=0] + +A custom argument resolver could be implemented like this: -[[rest-http-interface-return-values]] +include-code::./CustomHttpServiceArgumentResolver[tag=argumentresolver,indent=0] + +To configure the custom argument resolver: + +include-code::./CustomHttpServiceArgumentResolver[tag=usage,indent=0] + +TIP: By default, `RequestEntity` is not supported as a method parameter, instead encouraging +the use of more fine-grained method parameters for individual parts of the request. + + + +[[rest-http-service-client-return-values]] === Return Values The supported return values depend on the underlying client. @@ -1112,65 +1195,206 @@ depends on how the underlying HTTP client is configured. You can set a `blockTim value on the adapter level as well, but we recommend relying on timeout settings of the underlying HTTP client, which operates at a lower level and provides more control. +`RestClientAdapter` provides supports additional support for a return value of type +`InputStream` or `ResponseEntity` that provides access to the raw response +body content. -[[rest-http-interface-exceptions]] +[[rest-http-service-client-exceptions]] === Error Handling -To customize error response handling, you need to configure the underlying HTTP client. - -For `RestClient`: - -By default, `RestClient` raises `RestClientException` for 4xx and 5xx HTTP status codes. -To customize this, register a response status handler that applies to all responses -performed through the client: +To customize error handling for HTTP Service client proxies, you can configure the +underlying client as needed. By default, clients raise an exception for 4xx and 5xx HTTP +status codes. To customize this, register a response status handler that applies to all +responses performed through the client as follows: [source,java,indent=0,subs="verbatim,quotes"] ---- + // For RestClient RestClient restClient = RestClient.builder() .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...) .build(); - RestClientAdapter adapter = RestClientAdapter.create(restClient); + + // or for WebClient... + WebClient webClient = WebClient.builder() + .defaultStatusHandler(HttpStatusCode::isError, resp -> ...) + .build(); + WebClientAdapter adapter = WebClientAdapter.create(webClient); + + // or for RestTemplate... + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setErrorHandler(myErrorHandler); + + RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); ---- -For more details and options, such as suppressing error status codes, see the Javadoc of -`defaultStatusHandler` in `RestClient.Builder`. +For more details and options such as suppressing error status codes, see the reference +documentation for each client, as well as the Javadoc of `defaultStatusHandler` in +`RestClient.Builder` or `WebClient.Builder`, and the `setErrorHandler` of `RestTemplate`. + + + +[[rest-http-service-client-adapter-decorator]] +=== Decorating the Adapter -For `WebClient`: +`HttpExchangeAdapter` and `ReactorHttpExchangeAdapter` are contracts that decouple HTTP +Interface client infrastructure from the details of invoking the underlying +client. There are adapter implementations for `RestClient`, `WebClient`, and +`RestTemplate`. -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: +Occasionally, it may be useful to intercept client invocations through a decorator +configurable in the `HttpServiceProxyFactory.Builder`. For example, you can apply +built-in decorators to suppress 404 exceptions and return a `ResponseEntity` with +`NOT_FOUND` and a `null` body: [source,java,indent=0,subs="verbatim,quotes"] ---- - WebClient webClient = WebClient.builder() - .defaultStatusHandler(HttpStatusCode::isError, resp -> ...) + // For RestClient + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(restClientAdapter) + .exchangeAdapterDecorator(NotFoundRestClientAdapterDecorator::new) .build(); - WebClientAdapter adapter = WebClientAdapter.create(webClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(adapter).build(); + // or for WebClient... + HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builderFor(webClientAdapter) + .exchangeAdapterDecorator(NotFoundWebClientAdapterDecorator::new) + .build(); +---- + + + +[[rest-http-service-client-group-config]] +=== HTTP Service Groups + +It's trivial to create client proxies with `HttpServiceProxyFactory`, but to have them +declared as beans leads to repetitive configuration. You may also have multiple +target hosts, and therefore multiple clients to configure, and even more client proxy +beans to create. + +To make it easier to work with interface clients at scale the Spring Framework provides +dedicated configuration support. It lets applications focus on identifying HTTP Services +by group, and customizing the client for each group, while the framework transparently +creates a registry of client proxies, and declares each proxy as a bean. + +An HTTP Service group is simply a set of interfaces that share the same client setup and +`HttpServiceProxyFactory` instance to create proxies. Typically, that means one group per +host, but you can have more than one group for the same target host in case the +underlying client needs to be configured differently. + +One way to declare HTTP Service groups is via `@ImportHttpServices` annotations in +`@Configuration` classes as shown below: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) // <1> + @ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) // <2> + public class ClientConfig { + } + +---- +<1> Manually list interfaces for group "echo" +<2> Detect interfaces for group "greeting" under a base package + +It is also possible to declare groups programmatically by creating an HTTP Service +registrar and then importing it: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class MyHttpServiceRegistrar extends AbstractHttpServiceRegistrar { // <1> + + @Override + protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { + registry.forGroup("echo").register(EchoServiceA.class, EchoServiceB.class); // <2> + registry.forGroup("greeting").detectInBasePackages(GreetServiceA.class); // <3> + } + } + + @Configuration + @Import(MyHttpServiceRegistrar.class) // <4> + public class ClientConfig { + } + ---- +<1> Create extension class of `AbstractHttpServiceRegistrar` +<2> Manually list interfaces for group "echo" +<3> Detect interfaces for group "greeting" under a base package +<4> Import the registrar + +TIP: You can mix and match `@ImportHttpService` annotations with programmatic registrars, +and you can spread the imports across multiple configuration classes. All imports +contribute collaboratively the same, shared `HttpServiceProxyRegistry` instance. -For more details and options, such as suppressing error status codes, see the Javadoc of -`defaultStatusHandler` in `WebClient.Builder`. +Once HTTP Service groups are declared, add an `HttpServiceGroupConfigurer` bean to +customize the client for each group. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) + @ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) + public class ClientConfig { + + @Bean + public RestClientHttpServiceGroupConfigurer groupConfigurer() { + return groups -> { + // configure client for group "echo" + groups.filterByName("echo").forEachClient((group, clientBuilder) -> ...); + + // configure the clients for all groups + groups.forEachClient((group, clientBuilder) -> ...); + + // configure client and proxy factory for each group + groups.forEachGroup((group, clientBuilder, factoryBuilder) -> ...); + }; + } + } +---- -For `RestTemplate`: +TIP: Spring Boot uses an `HttpServiceGroupConfigurer` to add support for client properties +by HTTP Service group, Spring Security to add OAuth support, and Spring Cloud to add load +balancing. -By default, `RestTemplate` raises `RestClientException` for 4xx and 5xx HTTP status codes. -To customize this, register an error handler that applies to all responses -performed through the client: +As a result of the above, each client proxy is available as a bean that you can +conveniently autowire by type: [source,java,indent=0,subs="verbatim,quotes"] ---- - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setErrorHandler(myErrorHandler); + @RestController + public class EchoController { - RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); + private final EchoService echoService; + + public EchoController(EchoService echoService) { + this.echoService = echoService; + } + + // ... + } ---- -For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and -the `ResponseErrorHandler` hierarchy. +However, if there are multiple client proxies of the same type, e.g. the same interface +in multiple groups, then there is no unique bean of that type, and you cannot autowire by +type only. For such cases, you can work directly with the `HttpServiceProxyRegistry` that +holds all proxies, and obtain the ones you need by group: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @RestController + public class EchoController { + private final EchoService echoService1; + + private final EchoService echoService2; + + public EchoController(HttpServiceProxyRegistry registry) { + this.echoService1 = registry.getClient("echo1", EchoService.class); // <1> + this.echoService2 = registry.getClient("echo2", EchoService.class); // <2> + } + + // ... + } +---- +<1> Access the `EchoService` client proxy for group "echo1" +<2> Access the `EchoService` client proxy for group "echo2" diff --git a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc index e78997fa4ad0..76d8606348d7 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]] @@ -89,6 +90,22 @@ To configure the rules that the `TaskExecutor` uses, we expose simple bean prope 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]] == The Spring `TaskScheduler` Abstraction @@ -466,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). ==== @@ -516,8 +533,7 @@ that returns a value: ---- TIP: `@Async` methods may not only declare a regular `java.util.concurrent.Future` return -type but also Spring's `org.springframework.util.concurrent.ListenableFuture` or, as of -Spring 4.2, JDK 8's `java.util.concurrent.CompletableFuture`, for richer interaction with +type but also `java.util.concurrent.CompletableFuture`, for richer interaction with the asynchronous task and for immediate composition with further processing steps. You can not use `@Async` in conjunction with lifecycle callbacks such as `@PostConstruct`. @@ -616,7 +632,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: @@ -674,7 +690,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 @@ -727,7 +743,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 @@ -1046,4 +1062,3 @@ it is therefore not recommended to specify values at both levels. For example, d an "org.quartz.jobStore.class" property if you mean to rely on a Spring-provided DataSource, or specify an `org.springframework.scheduling.quartz.LocalDataSourceJobStore` variant which is a full-fledged replacement for the standard `org.quartz.impl.jdbcjobstore.JobStoreTX`. - diff --git a/framework-docs/modules/ROOT/pages/languages.adoc b/framework-docs/modules/ROOT/pages/languages.adoc index ec451606e984..1f532ea9c1cf 100644 --- a/framework-docs/modules/ROOT/pages/languages.adoc +++ b/framework-docs/modules/ROOT/pages/languages.adoc @@ -1,8 +1,3 @@ [[languages]] = Language Support :page-section-summary-toc: 1 - - - - - diff --git a/framework-docs/modules/ROOT/pages/languages/dynamic.adoc b/framework-docs/modules/ROOT/pages/languages/dynamic.adoc deleted file mode 100644 index fed4d8574e70..000000000000 --- a/framework-docs/modules/ROOT/pages/languages/dynamic.adoc +++ /dev/null @@ -1,858 +0,0 @@ -[[dynamic-language]] -= Dynamic Language Support - -Spring provides comprehensive support for using classes and objects that have been -defined by using a dynamic language (such as Groovy) with Spring. This support lets -you write any number of classes in a supported dynamic language and have the Spring -container transparently instantiate, configure, and dependency inject the resulting -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. - -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]. - - - - -[[dynamic-language-a-first-example]] -== A First Example - -The bulk of this chapter is concerned with describing the dynamic language support in -detail. Before diving into all of the ins and outs of the dynamic language support, -we look at a quick example of a bean defined in a dynamic language. The dynamic -language for this first bean is Groovy. (The basis of this example was taken from the -Spring test suite. If you want to see equivalent examples in any of the other -supported languages, take a look at the source code). - -The next example shows the `Messenger` interface, which the Groovy bean is going to -implement. Note that this interface is defined in plain Java. Dependent objects that -are injected with a reference to the `Messenger` do not know that the underlying -implementation is a Groovy script. The following listing shows the `Messenger` interface: - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - public interface Messenger { - - String getMessage(); - } ----- - -The following example defines a class that has a dependency on the `Messenger` interface: - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - public class DefaultBookingService implements BookingService { - - private Messenger messenger; - - public void setMessenger(Messenger messenger) { - this.messenger = messenger; - } - - public void processBooking() { - // use the injected Messenger object... - } - } ----- - -The following example implements the `Messenger` interface in Groovy: - -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages",fold="none"] ----- - package org.springframework.scripting.groovy - - // Import the Messenger interface (written in Java) that is to be implemented - import org.springframework.scripting.Messenger - - // Define the implementation in Groovy in file 'Messenger.groovy' - class GroovyMessenger implements Messenger { - - String message - } ----- - -[NOTE] -==== -To use the custom dynamic language tags to define dynamic-language-backed beans, you -need to have the XML Schema preamble at the top of your Spring XML configuration file. -You also need to use a Spring `ApplicationContext` implementation as your IoC -container. Using the dynamic-language-backed beans with a plain `BeanFactory` -implementation is supported, but you have to manage the plumbing of the Spring internals -to do so. - -For more information on schema-based configuration, see xref:languages/dynamic.adoc#xsd-schemas-lang[XML Schema-based Configuration] -. -==== - -Finally, the following example shows the bean definitions that effect the injection of the -Groovy-defined `Messenger` implementation into an instance of the -`DefaultBookingService` class: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - ----- - -The `bookingService` bean (a `DefaultBookingService`) can now use its private `messenger` -member variable as normal, because the `Messenger` instance that was injected into it is -a `Messenger` instance. There is nothing special going on here -- just plain Java and -plain Groovy. - -Hopefully, the preceding XML snippet is self-explanatory, but do not worry unduly if it is not. -Keep reading for the in-depth detail on the whys and wherefores of the preceding configuration. - - - - -[[dynamic-language-beans]] -== Defining Beans that Are Backed by Dynamic Languages - -This section describes exactly how you define Spring-managed beans in any of the -supported dynamic languages. - -Note that this chapter does not attempt to explain the syntax and idioms of the supported -dynamic languages. For example, if you want to use Groovy to write certain of the classes -in your application, we assume that you already know Groovy. If you need further details -about the dynamic languages themselves, see xref:languages/dynamic.adoc#dynamic-language-resources[Further Resources] at the end of -this chapter. - - - -[[dynamic-language-beans-concepts]] -=== Common Concepts - -The steps involved in using dynamic-language-backed beans are as follows: - -. Write the test for the dynamic language source code (naturally). -. Then write the dynamic language source code itself. -. Define your dynamic-language-backed beans by using the appropriate `` - element in the XML configuration (you can define such beans programmatically by - using the Spring API, although you will have to consult the source code for - directions on how to do this, as this chapter does not cover this type of advanced configuration). - Note that this is an iterative step. You need at least one bean definition for each dynamic - language source file (although multiple bean definitions can reference the same source file). - -The first two steps (testing and writing your dynamic language source files) are beyond -the scope of this chapter. See the language specification and reference manual -for your chosen dynamic language and crack on with developing your dynamic language -source files. You first want to read the rest of this chapter, though, as -Spring's dynamic language support does make some (small) assumptions about the contents -of your dynamic language source files. - - -[[dynamic-language-beans-concepts-xml-language-element]] -==== The element - -The final step in the list in the xref:languages/dynamic.adoc#dynamic-language-beans-concepts[preceding section] -involves defining dynamic-language-backed bean definitions, one for each bean that you -want to configure (this is no different from normal JavaBean configuration). However, -instead of specifying the fully qualified class name of the class that is to be -instantiated and configured by the container, you can use the `` -element to define the dynamic language-backed bean. - -Each of the supported languages has a corresponding `` element: - -* `` (Groovy) -* `` (BeanShell) -* `` (JSR-223, e.g. 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 -later in this chapter detail this). - - -[[dynamic-language-refreshable-beans]] -==== Refreshable Beans - -One of the (and perhaps the single) most compelling value adds of the dynamic language -support in Spring is the "`refreshable bean`" feature. - -A refreshable bean is a dynamic-language-backed bean. With a small amount of -configuration, a dynamic-language-backed bean can monitor changes in its underlying -source file resource and then reload itself when the dynamic language source file is -changed (for example, when you edit and save changes to the file on the file system). - -This lets you deploy any number of dynamic language source files as part of an -application, configure the Spring container to create beans backed by dynamic -language source files (using the mechanisms described in this chapter), and (later, -as requirements change or some other external factor comes into play) edit a dynamic -language source file and have any change they make be reflected in the bean that is -backed by the changed dynamic language source file. There is no need to shut down a -running application (or redeploy in the case of a web application). The -dynamic-language-backed bean so amended picks up the new state and logic from the -changed dynamic language source file. - -NOTE: This feature is off by default. - -Now we can take a look at an example to see how easy it is to start using refreshable -beans. To turn on the refreshable beans feature, you have to specify exactly one -additional attribute on the `` element of your bean definition. So, -if we stick with xref:languages/dynamic.adoc#dynamic-language-a-first-example[the example] from earlier in -this chapter, the following example shows what we would change in the Spring XML -configuration to effect refreshable beans: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - script-source="classpath:Messenger.groovy"> - - - - - - - - ----- - -That really is all you have to do. The `refresh-check-delay` attribute defined on the -`messenger` bean definition is the number of milliseconds after which the bean is -refreshed with any changes made to the underlying dynamic language source file. -You can turn off the refresh behavior by assigning a negative value to the -`refresh-check-delay` attribute. Remember that, by default, the refresh behavior is -disabled. If you do not want the refresh behavior, do not define the attribute. - -If we then run the following application, we can exercise the refreshable feature. -(Please excuse the "`jumping-through-hoops-to-pause-the-execution`" shenanigans -in this next slice of code.) The `System.in.read()` call is only there so that the -execution of the program pauses while you (the developer in this scenario) go off -and edit the underlying dynamic language source file so that the refresh triggers -on the dynamic-language-backed bean when the program resumes execution. - -The following listing shows this sample application: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.context.ApplicationContext; - import org.springframework.context.support.ClassPathXmlApplicationContext; - import org.springframework.scripting.Messenger; - - public final class Boot { - - public static void main(final String[] args) throws Exception { - ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml"); - Messenger messenger = (Messenger) ctx.getBean("messenger"); - System.out.println(messenger.getMessage()); - // pause execution while I go off and make changes to the source file... - System.in.read(); - System.out.println(messenger.getMessage()); - } - } ----- - -Assume then, for the purposes of this example, that all calls to the `getMessage()` -method of `Messenger` implementations have to be changed such that the message is -surrounded by quotation marks. The following listing shows the changes that you -(the developer) should make to the `Messenger.groovy` source file when the -execution of the program is paused: - -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting - - class GroovyMessenger implements Messenger { - - private String message = "Bingo" - - public String getMessage() { - // change the implementation to surround the message in quotes - return "'" + this.message + "'" - } - - public void setMessage(String message) { - this.message = message - } - } ----- - -When the program runs, the output before the input pause will be `I Can Do The Frug`. -After the change to the source file is made and saved and the program resumes execution, -the result of calling the `getMessage()` method on the dynamic-language-backed -`Messenger` implementation is `'I Can Do The Frug'` (notice the inclusion of the -additional quotation marks). - -Changes to a script do not trigger a refresh if the changes occur within the window of -the `refresh-check-delay` value. Changes to the script are not actually picked up until -a method is called on the dynamic-language-backed bean. It is only when a method is -called on a dynamic-language-backed bean that it checks to see if its underlying script -source has changed. Any exceptions that relate to refreshing the script (such as -encountering a compilation error or finding that the script file has been deleted) -results in a fatal exception being propagated to the calling code. - -The refreshable bean behavior described earlier does not apply to dynamic language -source files defined with the `` element notation (see -xref:languages/dynamic.adoc#dynamic-language-beans-inline[Inline Dynamic Language Source Files]). Additionally, it applies only to beans where -changes to the underlying source file can actually be detected (for example, by code -that checks the last modified date of a dynamic language source file that exists on the -file system). - - -[[dynamic-language-beans-inline]] -==== Inline Dynamic Language Source Files - -The dynamic language support can also cater to dynamic language source files that are -embedded directly in Spring bean definitions. More specifically, the -`` element lets you define dynamic language source immediately -inside a Spring configuration file. An example might clarify how the inline script -feature works: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - package org.springframework.scripting.groovy - - import org.springframework.scripting.Messenger - - class GroovyMessenger implements Messenger { - String message - } - - - - ----- - -If we put to one side the issues surrounding whether it is good practice to define -dynamic language source inside a Spring configuration file, the `` -element can be useful in some scenarios. For instance, we might want to quickly add a -Spring `Validator` implementation to a Spring MVC `Controller`. This is but a moment's -work using inline source. (See xref:languages/dynamic.adoc#dynamic-language-scenarios-validators[Scripted Validators] for such an -example.) - - -[[dynamic-language-beans-ctor-injection]] -==== Understanding Constructor Injection in the Context of Dynamic-language-backed Beans - -There is one very important thing to be aware of with regard to Spring's dynamic -language support. Namely, you can not (currently) supply constructor arguments -to dynamic-language-backed beans (and, hence, constructor-injection is not available for -dynamic-language-backed beans). In the interests of making this special handling of -constructors and properties 100% clear, the following mixture of code and configuration -does not work: - -.An approach that cannot work -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting.groovy - - import org.springframework.scripting.Messenger - - // from the file 'Messenger.groovy' - class GroovyMessenger implements Messenger { - - GroovyMessenger() {} - - // this constructor is not available for Constructor Injection - GroovyMessenger(String message) { - this.message = message; - } - - String message - - String anotherMessage - } ----- - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - ----- - -In practice this limitation is not as significant as it first appears, since setter -injection is the injection style favored by the overwhelming majority of developers -(we leave the discussion as to whether that is a good thing to another day). - - - -[[dynamic-language-beans-groovy]] -=== Groovy Beans - -This section describes how to use beans defined in Groovy in Spring. - -The Groovy homepage includes the following description: - -"`Groovy is an agile dynamic language for the Java 2 Platform that has many of the -features that people like so much in languages like Python, Ruby and Smalltalk, making -them available to Java developers using a Java-like syntax.`" - -If you have read this chapter straight from the top, you have already -xref:languages/dynamic.adoc#dynamic-language-a-first-example[seen an example] of a Groovy-dynamic-language-backed -bean. Now consider another example (again using an example from the Spring test suite): - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - public interface Calculator { - - int add(int x, int y); - } ----- - -The following example implements the `Calculator` interface in Groovy: - -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting.groovy - - // from the file 'calculator.groovy' - class GroovyCalculator implements Calculator { - - int add(int x, int y) { - x + y - } - } ----- - -The following bean definition uses the calculator defined in Groovy: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - ----- - -Finally, the following small application exercises the preceding configuration: - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - import org.springframework.context.ApplicationContext; - import org.springframework.context.support.ClassPathXmlApplicationContext; - - public class Main { - - public static void main(String[] args) { - ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml"); - Calculator calc = ctx.getBean("calculator", Calculator.class); - System.out.println(calc.add(2, 8)); - } - } ----- - -The resulting output from running the above program is (unsurprisingly) `10`. -(For more interesting examples, see the dynamic language showcase project for a more -complex example or see the examples xref:languages/dynamic.adoc#dynamic-language-scenarios[Scenarios] later in this chapter). - -You must not define more than one class per Groovy source file. While this is perfectly -legal in Groovy, it is (arguably) a bad practice. In the interests of a consistent -approach, you should (in the opinion of the Spring team) respect the standard Java -conventions of one (public) class per source file. - - -[[dynamic-language-beans-groovy-customizer]] -==== Customizing Groovy Objects by Using a Callback - -The `GroovyObjectCustomizer` interface is a callback that lets you hook additional -creation logic into the process of creating a Groovy-backed bean. For example, -implementations of this interface could invoke any required initialization methods, -set some default property values, or specify a custom `MetaClass`. The following listing -shows the `GroovyObjectCustomizer` interface definition: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - public interface GroovyObjectCustomizer { - - void customize(GroovyObject goo); - } ----- - -The Spring Framework instantiates an instance of your Groovy-backed bean and then -passes the created `GroovyObject` to the specified `GroovyObjectCustomizer` (if one -has been defined). You can do whatever you like with the supplied `GroovyObject` -reference. We expect that most people want to set a custom `MetaClass` with this -callback, and the following example shows how to do so: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - public final class SimpleMethodTracingCustomizer implements GroovyObjectCustomizer { - - public void customize(GroovyObject goo) { - DelegatingMetaClass metaClass = new DelegatingMetaClass(goo.getMetaClass()) { - - public Object invokeMethod(Object object, String methodName, Object[] arguments) { - System.out.println("Invoking '" + methodName + "'."); - return super.invokeMethod(object, methodName, arguments); - } - }; - metaClass.initialize(); - goo.setMetaClass(metaClass); - } - - } ----- - -A full discussion of meta-programming in Groovy is beyond the scope of the Spring -reference manual. See the relevant section of the Groovy reference manual or do a -search online. Plenty of articles address this topic. Actually, making use of a -`GroovyObjectCustomizer` is easy if you use the Spring namespace support, as the -following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - ----- - -If you do not use the Spring namespace support, you can still use the -`GroovyObjectCustomizer` functionality, as the following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - ----- - -NOTE: You may also specify a Groovy `CompilationCustomizer` (such as an `ImportCustomizer`) -or even a full Groovy `CompilerConfiguration` object in the same place as Spring's -`GroovyObjectCustomizer`. Furthermore, you may set a common `GroovyClassLoader` with custom -configuration for your beans at the `ConfigurableApplicationContext.setClassLoader` level; -this also leads to shared `GroovyClassLoader` usage and is therefore recommendable in case of -a large number of scripted beans (avoiding an isolated `GroovyClassLoader` instance per bean). - - - -[[dynamic-language-beans-bsh]] -=== BeanShell Beans - -This section describes how to use BeanShell beans in Spring. - -The https://beanshell.github.io/intro.html[BeanShell homepage] includes the following -description: - ----- -BeanShell is a small, free, embeddable Java source interpreter with dynamic language -features, written in Java. BeanShell dynamically runs standard Java syntax and -extends it with common scripting conveniences such as loose types, commands, and method -closures like those in Perl and JavaScript. ----- - -In contrast to Groovy, BeanShell-backed bean definitions require some (small) additional -configuration. The implementation of the BeanShell dynamic language support in Spring is -interesting, because Spring creates a JDK dynamic proxy that implements all of the -interfaces that are specified in the `script-interfaces` attribute value of the -`` element (this is why you must supply at least one interface in the value -of the attribute, and, consequently, program to interfaces when you use BeanShell-backed -beans). This means that every method call on a BeanShell-backed object goes through the -JDK dynamic proxy invocation mechanism. - -Now we can show a fully working example of using a BeanShell-based bean that implements -the `Messenger` interface that was defined earlier in this chapter. We again show the -definition of the `Messenger` interface: - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - public interface Messenger { - - String getMessage(); - } ----- - -The following example shows the BeanShell "`implementation`" (we use the term loosely here) -of the `Messenger` interface: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - String message; - - String getMessage() { - return message; - } - - void setMessage(String aMessage) { - message = aMessage; - } ----- - -The following example shows the Spring XML that defines an "`instance`" of the above -"`class`" (again, we use these terms very loosely here): - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - ----- - -See xref:languages/dynamic.adoc#dynamic-language-scenarios[Scenarios] for some scenarios where you might want to use -BeanShell-based beans. - - - - -[[dynamic-language-scenarios]] -== Scenarios - -The possible scenarios where defining Spring managed beans in a scripting language would -be beneficial are many and varied. This section describes two possible use cases for the -dynamic language support in Spring. - - - -[[dynamic-language-scenarios-controllers]] -=== Scripted Spring MVC Controllers - -One group of classes that can benefit from using dynamic-language-backed beans is that -of Spring MVC controllers. In pure Spring MVC applications, the navigational flow -through a web application is, to a large extent, determined by code encapsulated within -your Spring MVC controllers. As the navigational flow and other presentation layer logic -of a web application needs to be updated to respond to support issues or changing -business requirements, it may well be easier to effect any such required changes by -editing one or more dynamic language source files and seeing those changes being -immediately reflected in the state of a running application. - -Remember that, in the lightweight architectural model espoused by projects such as -Spring, you typically aim to have a really thin presentation layer, with all -the meaty business logic of an application being contained in the domain and service -layer classes. Developing Spring MVC controllers as dynamic-language-backed beans lets -you change presentation layer logic by editing and saving text files. Any -changes to such dynamic language source files is (depending on the configuration) -automatically reflected in the beans that are backed by dynamic language source files. - -NOTE: To effect this automatic "`pickup`" of any changes to dynamic-language-backed -beans, you have to enable the "`refreshable beans`" functionality. See -xref:languages/dynamic.adoc#dynamic-language-refreshable-beans[Refreshable Beans] for a full treatment of this feature. - -The following example shows an `org.springframework.web.servlet.mvc.Controller` implemented -by using the Groovy dynamic language: - -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.showcase.fortune.web - - import org.springframework.showcase.fortune.service.FortuneService - import org.springframework.showcase.fortune.domain.Fortune - import org.springframework.web.servlet.ModelAndView - import org.springframework.web.servlet.mvc.Controller - - import jakarta.servlet.http.HttpServletRequest - import jakarta.servlet.http.HttpServletResponse - - // from the file '/WEB-INF/groovy/FortuneController.groovy' - class FortuneController implements Controller { - - @Property FortuneService fortuneService - - ModelAndView handleRequest(HttpServletRequest request, - HttpServletResponse httpServletResponse) { - return new ModelAndView("tell", "fortune", this.fortuneService.tellFortune()) - } - } ----- - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - ----- - - - -[[dynamic-language-scenarios-validators]] -=== Scripted Validators - -Another area of application development with Spring that may benefit from the -flexibility afforded by dynamic-language-backed beans is that of validation. It can -be easier to express complex validation logic by using a loosely typed dynamic language -(that may also have support for inline regular expressions) as opposed to regular Java. - -Again, developing validators as dynamic-language-backed beans lets you change -validation logic by editing and saving a simple text file. Any such changes is -(depending on the configuration) automatically reflected in the execution of a -running application and would not require the restart of an application. - -NOTE: To effect the automatic "`pickup`" of any changes to dynamic-language-backed -beans, you have to enable the 'refreshable beans' feature. See -xref:languages/dynamic.adoc#dynamic-language-refreshable-beans[Refreshable Beans] for a full and detailed treatment of this feature. - -The following example shows a Spring `org.springframework.validation.Validator` -implemented by using the Groovy dynamic language (see xref:core/validation/validator.adoc[Validation using Spring’s Validator interface] - for a discussion of the -`Validator` interface): - -[source,groovy,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.validation.Validator - import org.springframework.validation.Errors - import org.springframework.beans.TestBean - - class TestBeanValidator implements Validator { - - boolean supports(Class clazz) { - return TestBean.class.isAssignableFrom(clazz) - } - - void validate(Object bean, Errors errors) { - if(bean.name?.trim()?.size() > 0) { - return - } - errors.reject("whitespace", "Cannot be composed wholly of whitespace.") - } - } ----- - - - - -[[dynamic-language-final-notes]] -== Additional Details - -This last section contains some additional details related to the dynamic language support. - - - -[[dynamic-language-final-notes-aop]] -=== AOP -- Advising Scripted Beans - -You can use the Spring AOP framework to advise scripted beans. The Spring AOP -framework actually is unaware that a bean that is being advised might be a scripted -bean, so all of the AOP use cases and functionality that you use (or aim to use) -work with scripted beans. When you advise scripted beans, you cannot use class-based -proxies. You must use xref:core/aop/proxying.adoc[interface-based proxies]. - -You are not limited to advising scripted beans. You can also write aspects themselves -in a supported dynamic language and use such beans to advise other Spring beans. -This really would be an advanced use of the dynamic language support though. - - - -[[dynamic-language-final-notes-scopes]] -=== Scoping - -In case it is not immediately obvious, scripted beans can be scoped in the same way as -any other bean. The `scope` attribute on the various `` elements lets -you control the scope of the underlying scripted bean, as it does with a regular -bean. (The default scope is xref:core/beans/factory-scopes.adoc#beans-factory-scopes-singleton[singleton], -as it is with "`regular`" beans.) - -The following example uses the `scope` attribute to define a Groovy bean scoped as -a xref:core/beans/factory-scopes.adoc#beans-factory-scopes-prototype[prototype]: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - ----- - -See xref:core/beans/factory-scopes.adoc[Bean Scopes] in xref:web/webmvc-view/mvc-xslt.adoc#mvc-view-xslt-beandefs[The IoC Container] -for a full discussion of the scoping support in the Spring Framework. - - - -[[xsd-schemas-lang]] -=== The `lang` XML schema - -The `lang` elements in Spring XML configuration deal with exposing objects that have been -written in a dynamic language (such as Groovy or BeanShell) as beans in the Spring container. - -These elements (and the dynamic language support) are comprehensively covered in -xref:languages/dynamic.adoc[Dynamic Language Support]. See that section -for full details on this support and the `lang` elements. - -To use the elements in the `lang` schema, you need to have the following preamble at the -top of your Spring XML configuration file. The text in the following snippet references -the correct schema so that the tags in the `lang` namespace are available to you: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - ----- - - - - -[[dynamic-language-resources]] -== Further Resources - -The following links go to further resources about the various dynamic languages referenced -in this chapter: - -* The https://www.groovy-lang.org/[Groovy] homepage -* The https://beanshell.github.io/intro.html[BeanShell] homepage -* The https://www.jruby.org[JRuby] homepage diff --git a/framework-docs/modules/ROOT/pages/languages/groovy.adoc b/framework-docs/modules/ROOT/pages/languages/groovy.adoc index e50f136b23bf..a552fdf61eab 100644 --- a/framework-docs/modules/ROOT/pages/languages/groovy.adoc +++ b/framework-docs/modules/ROOT/pages/languages/groovy.adoc @@ -6,9 +6,37 @@ Groovy is a powerful, optionally typed, and dynamic language, with static-typing compilation capabilities. It offers a concise syntax and integrates smoothly with any existing Java application. +[[beans-factory-groovy]] +== The Groovy Bean Definition DSL + The Spring Framework provides a dedicated `ApplicationContext` that supports a Groovy-based -Bean Definition DSL. For more details, see -xref:core/beans/basics.adoc#beans-factory-groovy[The Groovy Bean Definition DSL]. +Bean Definition DSL, as known from the Grails framework. + +Typically, such configuration live in a ".groovy" file with the structure shown in the +following example: + +[source,groovy,indent=0,subs="verbatim,quotes"] +---- + beans { + dataSource(BasicDataSource) { + driverClassName = "org.hsqldb.jdbcDriver" + url = "jdbc:hsqldb:mem:grailsDB" + username = "sa" + password = "" + settings = [mynew:"setting"] + } + sessionFactory(SessionFactory) { + dataSource = dataSource + } + myService(MyService) { + nestedBean = { AnotherBean bean -> + dataSource = dataSource + } + } + } +---- + +This configuration style is largely equivalent to XML bean definitions and even +supports Spring's XML configuration namespaces. It also allows for importing XML +bean definition files through an `importBeans` directive. -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 d373497009bc..bd8b3e4fc179 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin.adoc @@ -13,14 +13,11 @@ 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 {spring-boot-docs}/boot-features-kotlin.html[dedicated Kotlin support]. +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]. +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 {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 813d2c106b3b..4b8e97805f46 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/annotations.adoc @@ -14,16 +14,12 @@ 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 +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 {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 deleted file mode 100644 index c2c2b9f246da..000000000000 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc +++ /dev/null @@ -1,114 +0,0 @@ -[[kotlin-bean-definition-dsl]] -= Bean Definition DSL - -Spring Framework supports registering beans in a functional way by using lambdas -as an alternative to XML or Java configuration (`@Configuration` and `@Bean`). In a nutshell, -it lets you register beans with a lambda that acts as a `FactoryBean`. -This mechanism is very efficient, as it does not require any reflection or CGLIB proxies. - -In Java, you can, for example, write the following: - -[source,java,indent=0] ----- - class Foo {} - - class Bar { - private final Foo foo; - public Bar(Foo foo) { - this.foo = foo; - } - } - - GenericApplicationContext context = new GenericApplicationContext(); - context.registerBean(Foo.class); - context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class))); ----- - -In Kotlin, with reified type parameters and `GenericApplicationContext` Kotlin extensions, -you can instead write the following: - -[source,kotlin,indent=0] ----- - class Foo - - class Bar(private val foo: Foo) - - val context = GenericApplicationContext().apply { - registerBean() - registerBean { Bar(it.getBean()) } - } ----- - -When the class `Bar` has a single constructor, you can even just specify the bean class, -the constructor parameters will be autowired by type: - -[source,kotlin,indent=0] ----- - val context = GenericApplicationContext().apply { - registerBean() - registerBean() - } ----- - -In order to allow a more declarative approach and cleaner syntax, Spring Framework provides -a {spring-framework-api-kdoc}/spring-context/org.springframework.context.support/-bean-definition-dsl/index.html[Kotlin bean definition DSL] -It declares an `ApplicationContextInitializer` through a clean declarative API, -which lets you deal with profiles and `Environment` for customizing -how beans are registered. - -In the following example notice that: - -* Type inference usually allows to avoid specifying the type for bean references like `ref("bazBean")` -* It is possible to use Kotlin top level functions to declare beans using callable references like `bean(::myRouter)` in this example -* When specifying `bean()` or `bean(::myRouter)`, parameters are autowired by type -* The `FooBar` bean will be registered only if the `foobar` profile is active - -[source,kotlin,indent=0] ----- - class Foo - class Bar(private val foo: Foo) - class Baz(var message: String = "") - class FooBar(private val baz: Baz) - - val myBeans = beans { - bean() - bean() - bean("bazBean") { - Baz().apply { - message = "Hello world" - } - } - profile("foobar") { - bean { FooBar(ref("bazBean")) } - } - bean(::myRouter) - } - - fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router { - // ... - } ----- - -NOTE: This DSL is programmatic, meaning it allows custom registration logic of beans -through an `if` expression, a `for` loop, or any other Kotlin constructs. - -You can then use this `beans()` function to register beans on the application context, -as the following example shows: - -[source,kotlin,indent=0] ----- - val context = GenericApplicationContext().apply { - myBeans.initialize(this) - refresh() - } ----- - -NOTE: Spring Boot is based on JavaConfig and -{spring-boot-issues}/8115[does not yet provide specific support for functional bean definition], -but you can experimentally use functional bean definitions through Spring Boot's `ApplicationContextInitializer` support. -See {stackoverflow-questions}/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685[this Stack Overflow answer] -for more details and up-to-date information. See also the experimental Kofu DSL developed in {spring-github-org}-experimental/spring-fu[Spring Fu incubator]. - - - - diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/bean-registration-dsl.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/bean-registration-dsl.adoc new file mode 100644 index 000000000000..da759af2bf0c --- /dev/null +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/bean-registration-dsl.adoc @@ -0,0 +1,4 @@ +[[kotlin-bean-registration-dsl]] += Bean Registration DSL + +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 604563704a50..fa63179bb764 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/classes-interfaces.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/classes-interfaces.adoc @@ -3,18 +3,16 @@ :page-section-summary-toc: 1 The Spring Framework supports various Kotlin constructs, such as instantiating Kotlin classes -through primary constructors, immutable classes data binding, and function optional parameters -with default values. +through primary constructors, data binding for immutable classes, and optional parameters +with default values for functions. Kotlin parameter names are recognized through a dedicated `KotlinReflectionParameterNameDiscoverer`, -which allows finding interface method parameter names without requiring the Java 8 `-parameters` -compiler flag to be enabled during compilation. (For completeness, we nevertheless recommend -running the Kotlin compiler with its `-java-parameters` flag for standard Java parameter exposure.) +which allows finding interface method parameter names without requiring the Java `-parameters` +compiler flag to be enabled during compilation. + +TIP: For completeness, we nevertheless recommend running the Kotlin compiler with its +`-java-parameters` flag for standard Java parameter exposure. You can declare configuration classes as {kotlin-docs}/nested-classes.html[top level or nested but not inner], -since the later requires a reference to the outer class. - - - - +since the latter 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 913acb052e71..c84eb033e5a2 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc @@ -1,8 +1,8 @@ [[coroutines]] = Coroutines -Kotlin {kotlin-docs}/coroutines-overview.html[Coroutines] are Kotlin -lightweight threads allowing to write non-blocking code in an imperative way. On language side, +Kotlin {kotlin-docs}/coroutines-overview.html[Coroutines] are instances of +suspendable computations 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 {kotlin-github-org}/kotlinx.coroutines[kotlinx.coroutines] provides functions like {kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines/async.html[`async { }`] @@ -20,7 +20,6 @@ Spring Framework provides support for Coroutines on the following scope: * Spring AOP - [[dependencies]] == Dependencies @@ -40,7 +39,6 @@ dependencies { Version `1.4.0` and above are supported. - [[how-reactive-translates-to-coroutines]] == How Reactive translates to Coroutines? @@ -69,7 +67,6 @@ Read this blog post about {spring-site-blog}/2019/04/12/going-reactive-with-spri for more details, including how to run code concurrently with Coroutines. - [[controllers]] == Controllers @@ -168,11 +165,12 @@ class CoroutinesViewController(banner: Banner) { ---- - [[webflux-fn]] == WebFlux.fn -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. +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] ---- @@ -204,56 +202,77 @@ class UserHandler(builder: WebClient.Builder) { ---- - [[transactions]] == 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. [source,kotlin,indent=0] ---- - import org.springframework.transaction.reactive.executeAndAwait + import org.springframework.transaction.reactive.executeAndAwait - class PersonRepository(private val operator: TransactionalOperator) { + class PersonRepository(private val operator: TransactionalOperator) { - suspend fun initDatabase() = operator.executeAndAwait { - insertPerson1() - insertPerson2() - } + suspend fun initDatabase() = operator.executeAndAwait { + insertPerson1() + insertPerson2() + } - private suspend fun insertPerson1() { - // INSERT SQL statement - } + private suspend fun insertPerson1() { + // INSERT SQL statement + } - private suspend fun insertPerson2() { - // INSERT SQL statement - } - } + private suspend fun insertPerson2() { + // INSERT SQL statement + } + } ---- For Kotlin `Flow`, a `Flow.transactional` extension is provided. [source,kotlin,indent=0] ---- - import org.springframework.transaction.reactive.transactional + import org.springframework.transaction.reactive.transactional - class PersonRepository(private val operator: TransactionalOperator) { + class PersonRepository(private val operator: TransactionalOperator) { - fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) + fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) - private fun findPeople(): Flow { - // SELECT SQL statement - } + private fun findPeople(): Flow { + // SELECT SQL statement + } - private suspend fun updatePerson(person: Person): Person { - // UPDATE SQL statement - } - } + private suspend fun updatePerson(person: Person): Person { + // UPDATE SQL statement + } + } ---- +[[coroutines.propagation]] +== Context Propagation + +Spring applications are xref:integration/observability.adoc[instrumented with Micrometer for Observability support]. +For tracing support, the current observation is propagated through a `ThreadLocal` for blocking code, +or the Reactor `Context` for reactive pipelines. But the current observation also needs to be made available +in the execution context of a suspended function. Without that, the current "traceId" will not be automatically +prepended to logged statements from coroutines. + +The {spring-framework-api-kdoc}/spring-core/org.springframework.core/-propagation-context-element/index.html[`PropagationContextElement`] operator generally ensures that the +{micrometer-context-propagation-docs}/[Micrometer Context Propagation library] works with Kotlin Coroutines. + +It requires the `io.micrometer:context-propagation` dependency and optionally the +`org.jetbrains.kotlinx:kotlinx-coroutines-reactor` one. Automatic context propagation via +`CoroutinesUtils#invokeSuspendingFunction` (used by Spring to adapt Coroutines to Reactor `Flux` or `Mono`) can be +enabled by invoking `Hooks.enableAutomaticContextPropagation()`. + +Applications can also use `PropagationContextElement` explicitly to augment the `CoroutineContext` +with the context propagation mechanism: +include-code::./ContextPropagationSample[tag=context,indent=0] +Here, assuming that Micrometer Tracing is configured, the resulting logging statement will show the current "traceId" +and unlock better observability for your application. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc index 6af9b086ae9f..e8c94e7560ab 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/extensions.adoc @@ -40,7 +40,3 @@ With Kotlin and the Spring Framework extensions, you can instead write the follo As in Java, `users` in Kotlin is strongly typed, but Kotlin's clever type inference allows for shorter syntax. - - - - 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 6b8b75b491ec..2430258ce89c 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/getting-started.adoc @@ -5,7 +5,6 @@ The easiest way to learn how to build a Spring application with Kotlin is to fol {spring-site-guides}/tutorials/spring-boot-kotlin/[the dedicated tutorial]. - [[start-spring-io]] == `start.spring.io` @@ -13,7 +12,6 @@ The easiest way to start a new Spring Framework project in Kotlin is to create a Boot project on https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin[start.spring.io]. - [[choosing-the-web-flavor]] == Choosing the Web Flavor @@ -25,7 +23,3 @@ long-lived connections or streaming scenarios. For other use cases, especially if you are using blocking technologies such as JPA, Spring 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 96070d163f42..213a04c9dfa6 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc @@ -5,34 +5,11 @@ One of Kotlin's key features is {kotlin-docs}/null-safety.html[null-safety], which cleanly deals with `null` values at compile time rather than bumping into the famous `NullPointerException` at runtime. This makes applications safer through nullability declarations and expressing "`value or no value`" semantics without paying the cost of wrappers, such as `Optional`. -(Kotlin allows using functional constructs with nullable values. See this -{baeldung-blog}/kotlin-null-safety[comprehensive guide to Kotlin null-safety].) +Kotlin allows using functional constructs with nullable values. See this +{baeldung-blog}/kotlin-null-safety[comprehensive guide to Kotlin null-safety]. Although Java does not let you express null-safety in its type-system, the Spring Framework -provides xref:languages/kotlin/null-safety.adoc[null-safety of the whole Spring Framework API] -via tooling-friendly annotations declared in the `org.springframework.lang` package. -By default, types from Java APIs used in Kotlin are recognized as -{kotlin-docs}/java-interop.html#null-safety-and-platform-types[platform types], -for which null-checks are relaxed. -{kotlin-docs}/java-interop.html#jsr-305-support[Kotlin support for JSR-305 annotations] -and Spring nullability annotations provide null-safety for the whole Spring Framework API to Kotlin developers, -with the advantage of dealing with `null`-related issues at compile time. - -NOTE: Libraries such as Reactor or Spring Data provide null-safe APIs to leverage this feature. - -You can configure JSR-305 checks by adding the `-Xjsr305` compiler flag with the following -options: `-Xjsr305={strict|warn|ignore}`. - -For kotlin versions 1.1+, the default behavior is the same as `-Xjsr305=warn`. -The `strict` value is required to have Spring Framework API null-safety taken into account -in Kotlin types inferred from Spring API but should be used with the knowledge that Spring -API nullability declaration could evolve even between minor releases and that more checks may -be added in the future. - -NOTE: Generic type arguments, varargs, and array elements nullability are not supported yet, -but should be in an upcoming release. See {kotlin-github-org}/KEEP/issues/79[this discussion] -for up-to-date information. - - - +provides xref:core/null-safety.adoc[null-safety of the whole Spring Framework API] +via tooling-friendly https://jspecify.dev/[JSpecify] annotations. +As of Kotlin 2.1, Kotlin enforces strict handling of nullability annotations from `org.jspecify.annotations` package. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc index d2b3657d3127..834ddace2998 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc @@ -2,7 +2,7 @@ = Requirements :page-section-summary-toc: 1 -Spring Framework supports Kotlin 1.7+ and requires +Spring Framework supports Kotlin 2.2+ and requires https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib[`kotlin-stdlib`] and https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-reflect[`kotlin-reflect`] to be present on the classpath. They are provided by default if you bootstrap a Kotlin project on @@ -12,7 +12,3 @@ NOTE: The {jackson-github-org}/jackson-module-kotlin[Jackson Kotlin module] is r 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 f3be082c275a..a8adbc915ff1 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/resources.adoc @@ -12,7 +12,6 @@ Kotlin and the Spring Framework: * https://kotlin.link/[Awesome Kotlin] - [[examples]] == Examples 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 300c0089c897..495ac3337a53 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 @@ -5,7 +5,6 @@ This section provides some specific hints and recommendations worth for developi in Kotlin. - [[final-by-default]] == Final by Default @@ -53,7 +52,6 @@ NOTE: The Kotlin code samples in Spring Framework documentation do not explicitl using the `kotlin-allopen` plugin, since this is the most commonly used setup. - [[using-immutable-class-instances-for-persistence]] == Using Immutable Class Instances for Persistence @@ -99,7 +97,6 @@ does not require the `kotlin-noarg` plugin if the module uses Spring Data object (such as MongoDB, Redis, Cassandra, and others). - [[injecting-dependencies]] == Injecting Dependencies @@ -175,6 +172,7 @@ As a consequence, the related bean name represented as a Kotlin string is `"samp 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 @@ -186,11 +184,11 @@ Therefore, if you wish to use the `@Value` annotation in Kotlin, you need to esc character by writing pass:q[`@Value("\${property}")`]. NOTE: If you use Spring Boot, you should probably use -{spring-boot-docs}/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 -following configuration beans: +following `PropertySourcesPlaceholderConfigurer` bean: [source,kotlin,indent=0] ---- @@ -200,8 +198,10 @@ following configuration beans: } ---- -You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`) -that uses the `${...}` syntax, with configuration beans, as the following example shows: +You can support components (such as Spring Boot actuators or `@LocalServerPort`) that use +the standard `${...}` syntax alongside components that use the custom `%{...}` syntax by +declaring multiple `PropertySourcesPlaceholderConfigurer` beans, as the following example +shows: [source,kotlin,indent=0] ---- @@ -215,6 +215,9 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer() ---- +In addition, the default escape character can be changed or disabled globally by setting +the `spring.placeholder.escapeCharacter.default` property via a JVM system property (or +via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism). [[checked-exceptions]] @@ -231,7 +234,6 @@ To get the original exception thrown like in Java, methods should be annotated w to specify explicitly the checked exceptions thrown (for example `@Throws(IOException::class)`). - [[annotation-array-attributes]] == Annotation Array Attributes @@ -278,7 +280,6 @@ NOTE: If the `@RequestMapping` `method` attribute is not specified, all HTTP met be matched, not only the `GET` method. - [[declaration-site-variance]] == Declaration-site variance @@ -296,17 +297,17 @@ for example when writing a `org.springframework.core.convert.converter.Converter [source,kotlin,indent=0] ---- -class ListOfFooConverter : Converter, CustomJavaList> { - // ... -} + class ListOfFooConverter : Converter, CustomJavaList> { + // ... + } ---- When converting any kind of objects, star projection with `*` can be used instead of `out Any`. [source,kotlin,indent=0] ---- -class ListOfAnyConverter : Converter, CustomJavaList<*>> { - // ... -} + class ListOfAnyConverter : Converter, CustomJavaList<*>> { + // ... + } ---- NOTE: Spring Framework does not leverage yet declaration-site variance type information for injecting beans, @@ -314,23 +315,29 @@ subscribe to {spring-framework-issues}/22313[spring-framework#22313] to track re progresses. - [[testing]] == Testing This section addresses testing with the combination of Kotlin and Spring Framework. -The recommended testing framework is https://junit.org/junit5/[JUnit 5] along with +The recommended testing framework is https://junit.org/[JUnit] along with https://mockk.io/[Mockk] for mocking. -NOTE: If you are using Spring Boot, see -{spring-boot-docs}/features.html#features.kotlin.testing[this related documentation]. +[TIP] +==== +Kotlin lets you specify meaningful test function names between backticks (```). +For a concrete example, see the `+++`Find all users on HTML page`()+++` test function later +in this section. +==== + +NOTE: If you are using Spring Boot, see +{spring-boot-docs-ref}/features/kotlin.html#features.kotlin.testing[this related documentation]. [[constructor-injection]] === Constructor injection As described in the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[dedicated section], -JUnit Jupiter (JUnit 5) allows constructor injection of beans which is pretty useful with Kotlin +JUnit Jupiter allows constructor injection of beans which is pretty useful with Kotlin in order to use `val` instead of `lateinit var`. You can use {spring-framework-api}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`] to enable autowiring for all parameters. @@ -340,21 +347,20 @@ file with a `spring.test.constructor.autowire.mode = all` property. [source,kotlin,indent=0] ---- -@SpringJUnitConfig(TestConfig::class) -@TestConstructor(autowireMode = AutowireMode.ALL) -class OrderServiceIntegrationTests(val orderService: OrderService, - val customerService: CustomerService) { - - // tests that use the injected OrderService and CustomerService -} + @SpringJUnitConfig(TestConfig::class) + @TestConstructor(autowireMode = AutowireMode.ALL) + class OrderServiceIntegrationTests( + val orderService: OrderService, + val customerService: CustomerService) { + + // tests that use the injected OrderService and CustomerService + } ---- - [[per_class-lifecycle]] === `PER_CLASS` Lifecycle -Kotlin lets you specify meaningful test function names between backticks (```). -With JUnit Jupiter (JUnit 5), Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` +With JUnit Jupiter, Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` annotation to enable single instantiation of test classes, which allows the use of `@BeforeAll` and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin. @@ -368,61 +374,59 @@ The following example demonstrates `@BeforeAll` and `@AfterAll` annotations on n @TestInstance(TestInstance.Lifecycle.PER_CLASS) class IntegrationTests { - val application = Application(8181) - val client = WebClient.create("http://localhost:8181") - - @BeforeAll - fun beforeAll() { - application.start() - } - - @Test - fun `Find all users on HTML page`() { - client.get().uri("/users") - .accept(TEXT_HTML) - .retrieve() - .bodyToMono() - .test() - .expectNextMatches { it.contains("Foo") } - .verifyComplete() - } - - @AfterAll - fun afterAll() { - application.stop() - } + val application = Application(8181) + val client = WebClient.create("http://localhost:8181") + + @BeforeAll + fun beforeAll() { + application.start() + } + + @Test + fun `Find all users on HTML page`() { + client.get().uri("/users") + .accept(TEXT_HTML) + .retrieve() + .bodyToMono() + .test() + .expectNextMatches { it.contains("Foo") } + .verifyComplete() + } + + @AfterAll + fun afterAll() { + application.stop() + } } ---- - [[specification-like-tests]] === Specification-like Tests -You can create specification-like tests with JUnit 5 and Kotlin. -The following example shows how to do so: +You can create specification-like tests with Kotlin and JUnit Jupiter's `@Nested` test +class support. The following example shows how to do so: [source,kotlin,indent=0] ---- -class SpecificationLikeTests { - - @Nested - @DisplayName("a calculator") - inner class Calculator { - val calculator = SampleCalculator() - - @Test - fun `should return the result of adding the first number to the second number`() { - val sum = calculator.sum(2, 4) - assertEquals(6, sum) - } - - @Test - fun `should return the result of subtracting the second number from the first number`() { - val subtract = calculator.subtract(4, 2) - assertEquals(2, subtract) - } - } -} + class SpecificationLikeTests { + + @Nested + @DisplayName("a calculator") + inner class Calculator { + + val calculator = SampleCalculator() + + @Test + fun `should return the result of adding the first number to the second number`() { + val sum = calculator.sum(2, 4) + assertEquals(6, sum) + } + + @Test + fun `should return the result of subtracting the second number from the first number`() { + val subtract = calculator.subtract(4, 2) + assertEquals(2, subtract) + } + } + } ---- - - diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc index e594069b0d4a..86684be99f2e 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc @@ -2,7 +2,6 @@ = Web - [[router-dsl]] == Router DSL @@ -16,27 +15,27 @@ These DSL let you write clean and idiomatic Kotlin code to build a `RouterFuncti [source,kotlin,indent=0] ---- -@Configuration -class RouterRouterConfiguration { - - @Bean - fun mainRouter(userHandler: UserHandler) = router { - accept(TEXT_HTML).nest { - GET("/") { ok().render("index") } - GET("/sse") { ok().render("sse") } - GET("/users", userHandler::findAllView) - } - "/api".nest { - accept(APPLICATION_JSON).nest { - GET("/users", userHandler::findAll) + @Configuration + class RouterRouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = router { + accept(TEXT_HTML).nest { + GET("/") { ok().render("index") } + GET("/sse") { ok().render("sse") } + GET("/users", userHandler::findAllView) } - accept(TEXT_EVENT_STREAM).nest { - GET("/users", userHandler::stream) + "/api".nest { + accept(APPLICATION_JSON).nest { + GET("/users", userHandler::findAll) + } + accept(TEXT_EVENT_STREAM).nest { + GET("/users", userHandler::stream) + } } + resources("/**", ClassPathResource("static/")) } - resources("/**", ClassPathResource("static/")) } -} ---- NOTE: This DSL is programmatic, meaning that it allows custom registration logic of beans @@ -46,7 +45,6 @@ when you need to register routes depending on dynamic data (for example, from a See https://github.com/mixitconf/mixit/[MiXiT project] for a concrete example. - [[mockmvc-dsl]] == MockMvc DSL @@ -55,86 +53,32 @@ idiomatic Kotlin API and to allow better discoverability (no usage of static met [source,kotlin,indent=0] ---- -val mockMvc: MockMvc = ... -mockMvc.get("/person/{name}", "Lee") { - secure = true - accept = APPLICATION_JSON - headers { - contentLanguage = Locale.FRANCE - } - principal = Principal { "foo" } -}.andExpect { - status { isOk } - content { contentType(APPLICATION_JSON) } - jsonPath("$.name") { value("Lee") } - content { json("""{"someBoolean": false}""", false) } -}.andDo { - print() -} ----- - - - -[[kotlin-script-templates]] -== Kotlin Script Templates - -Spring Framework provides a -{spring-framework-api}/web/servlet/view/script/ScriptTemplateView.html[`ScriptTemplateView`] -which supports {JSR}223[JSR-223] to render templates by using script engines. - -By leveraging `scripting-jsr223` dependencies, it -is possible to use such feature to render Kotlin-based templates with -{kotlin-github-org}/kotlinx.html[kotlinx.html] DSL or Kotlin multiline interpolated `String`. - -`build.gradle.kts` -[source,kotlin,indent=0] ----- -dependencies { - runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}") -} ----- - -Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplateViewResolver` beans. - -`KotlinScriptConfiguration.kt` -[source,kotlin,indent=0] ----- -@Configuration -class KotlinScriptConfiguration { - - @Bean - fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply { - engineName = "kotlin" - setScripts("scripts/render.kts") - renderFunction = "render" - isSharedEngine = false + val mockMvc: MockMvc = ... + mockMvc.get("/person/{name}", "Lee") { + secure = true + accept = APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } + }.andExpect { + status { isOk } + content { contentType(APPLICATION_JSON) } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } + }.andDo { + print() } - - @Bean - fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply { - setPrefix("templates/") - setSuffix(".kts") - } -} ---- -See the https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] example -project for more details. - - [[kotlin-multiplatform-serialization]] == Kotlin multiplatform serialization {kotlin-github-org}/kotlinx.serialization[Kotlin multiplatform serialization] is -supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON, and ProtoBuf formats. - -To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin. -With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since -Kotlin serialization is designed to serialize only Kotlin classes annotated with `@Serializable`. -With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration, -if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually. - - - +supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON, +and ProtoBuf formats. +To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependencies +and plugin. With Spring MVC and WebFlux, Kotlin serialization is configured by default if it is in the classpath and +other variants like Jackson are not. If needed, configure the converters or codecs manually. diff --git a/framework-docs/modules/ROOT/pages/overview.adoc b/framework-docs/modules/ROOT/pages/overview.adoc index 9e002aea0a82..8ac7c152c6b8 100644 --- a/framework-docs/modules/ROOT/pages/overview.adoc +++ b/framework-docs/modules/ROOT/pages/overview.adoc @@ -19,8 +19,6 @@ based on a diverse range of real-world use cases. This has helped Spring to succ evolve over a very long time. - - [[overview-spring]] == What We Mean by "Spring" @@ -37,14 +35,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. [[overview-history]] @@ -73,11 +69,11 @@ 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, -Jetty 11 and Undertow 2.3 as web servers, and also with Hibernate ORM 6.1. +Jetty 11 as web servers, and also with Hibernate ORM 6.1. Over time, the role of Java/Jakarta EE in application development has evolved. In the early days of J2EE and Spring, applications were created to be deployed to an application @@ -93,8 +89,6 @@ issue tracker, and release cadence. See {spring-site-projects}[spring.io/project the complete list of Spring projects. - - [[overview-philosophy]] == Design Philosophy @@ -119,8 +113,6 @@ meaningful, current, and accurate javadoc. It is one of very few projects that c clean code structure with no circular dependencies between packages. - - [[overview-feedback]] == Feedback and Contributions @@ -139,8 +131,6 @@ For more details see the guidelines at the {spring-framework-code}/CONTRIBUTING. top-level project page. - - [[overview-getting-started]] == Getting Started diff --git a/framework-docs/modules/ROOT/pages/rsocket.adoc b/framework-docs/modules/ROOT/pages/rsocket.adoc index 402c213898df..884a6ee447d3 100644 --- a/framework-docs/modules/ROOT/pages/rsocket.adoc +++ b/framework-docs/modules/ROOT/pages/rsocket.adoc @@ -44,8 +44,6 @@ and {reactor-github-org}/reactor-netty[Reactor Netty] for the transport. That me signals from Reactive Streams Publishers in your application propagate transparently through RSocket across the network. - - [[rsocket-protocol]] === The Protocol @@ -100,8 +98,6 @@ Protocol extensions define common metadata formats for use in applications: independently formatted metadata entries. * {rsocket-protocol-extensions}/Routing.md[Routing] -- the route for a request. - - [[rsocket-java]] === Java Implementation @@ -113,7 +109,7 @@ a natural fit to use `Flux` and `Mono` with declarative operators and transparen 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 @@ -130,8 +126,6 @@ with RSocket independent of Spring. The RSocket Java repository contains a numbe {rsocket-java-code}/rsocket-examples[sample apps] that demonstrate its API and protocol features. - - [[rsocket-spring]] === Spring Support @@ -163,7 +157,6 @@ clients and servers. See the Spring Integration Reference Manual for more detail Spring Cloud Gateway supports RSocket connections. - [[rsocket-requester]] == RSocketRequester @@ -171,7 +164,6 @@ Spring Cloud Gateway supports RSocket connections. returning objects for data and metadata instead of low level data buffers. It can be used symmetrically, to make requests from clients and to make requests from servers. - [[rsocket-requester-client]] === Client Requester @@ -186,7 +178,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 +188,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) @@ -208,7 +200,6 @@ Kotlin:: The above does not connect immediately. When requests are made, a shared connection is established transparently and used. - [[rsocket-requester-client-setup]] ==== Connection Setup @@ -226,10 +217,9 @@ metadata, the default mime type is 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, -xref:rsocket.adoc#rsocket-annot-connectmapping[@ConnectMapping] methods can be used to handle the start of a -connection and the content of the `SETUP` frame. Metadata may be used for connection -level security. - +xref:rsocket.adoc#rsocket-annot-connectmapping[@ConnectMapping] methods can be used to +handle the start of a connection and the content of the `SETUP` frame. Metadata may be +used for connection level security. [[rsocket-requester-client-strategies]] ==== Strategies @@ -244,11 +234,11 @@ 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())) - .decoders(decoders -> decoders.add(new Jackson2CborDecoder())) + .encoders(encoders -> encoders.add(new JacksonCborEncoder())) + .decoders(decoders -> decoders.add(new JacksonCborDecoder())) .build(); RSocketRequester requester = RSocketRequester.builder() @@ -258,11 +248,11 @@ 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()) } - .decoders { it.add(Jackson2CborDecoder()) } + .encoders { it.add(JacksonCborEncoder()) } + .decoders { it.add(JacksonCborDecoder()) } .build() val requester = RSocketRequester.builder() @@ -271,10 +261,9 @@ 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. - [[rsocket-requester-client-responder]] ==== Client Responders @@ -288,7 +277,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 +297,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 +324,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 +336,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,14 +350,13 @@ 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. See also xref:rsocket.adoc#rsocket-annot-responders[Annotated Responders], for more on the programming model. - [[rsocket-requester-client-advanced]] ==== Advanced @@ -381,7 +369,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 +380,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val requester = RSocketRequester.builder() .rsocketConnector { @@ -402,7 +390,6 @@ Kotlin:: ---- ====== - [[rsocket-requester-server]] === Server Requester @@ -419,7 +406,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 +423,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) { @@ -452,8 +439,6 @@ Kotlin:: <2> Perform handling in the suspending function. ====== - - [[rsocket-requester-requests]] === Requests @@ -464,7 +449,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 +464,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val viewBox: ViewBox = ... @@ -516,7 +501,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 +509,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 @@ -541,7 +526,7 @@ values are supported by a registered `Encoder`. For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String securityToken = ... ; ViewBox viewBox = ... ; @@ -555,7 +540,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 @@ -578,7 +563,6 @@ indicates only that the message was successfully sent, and not that it was handl For `Metadata-Push` use the `sendMetadata()` method with a `Mono` return value. - [[rsocket-annot-responders]] == Annotated Responders @@ -587,8 +571,6 @@ RSocket responders can be implemented as `@MessageMapping` and `@ConnectMapping` connection-level events (setup and metadata push). Annotated responders are supported symmetrically, for responding from the server side and for responding from the client side. - - [[rsocket-annot-responders-server]] === Server Responders @@ -600,7 +582,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 +598,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class ServerConfig { @@ -636,7 +618,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 +631,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 @@ -684,7 +666,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 { @@ -699,8 +681,8 @@ Java:: @Bean public RSocketStrategies rsocketStrategies() { return RSocketStrategies.builder() - .encoders(encoders -> encoders.add(new Jackson2CborEncoder())) - .decoders(decoders -> decoders.add(new Jackson2CborDecoder())) + .encoders(encoders -> encoders.add(new JacksonCborEncoder())) + .decoders(decoders -> decoders.add(new JacksonCborDecoder())) .routeMatcher(new PathPatternRouteMatcher()) .build(); } @@ -709,7 +691,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class ServerConfig { @@ -721,16 +703,14 @@ Kotlin:: @Bean fun rsocketStrategies() = RSocketStrategies.builder() - .encoders { it.add(Jackson2CborEncoder()) } - .decoders { it.add(Jackson2CborDecoder()) } + .encoders { it.add(JacksonCborEncoder()) } + .decoders { it.add(JacksonCborDecoder()) } .routeMatcher(PathPatternRouteMatcher()) .build() } ---- ====== - - [[rsocket-annot-responders-client]] === Client Responders @@ -738,8 +718,6 @@ Annotated responders on the client side need to be configured in the `RSocketRequester.Builder`. For details, see xref:rsocket.adoc#rsocket-requester-client-responder[Client Responders]. - - [[rsocket-annot-messagemapping]] === @MessageMapping @@ -751,7 +729,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 +743,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class RadarsController { @@ -798,7 +776,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` @@ -863,8 +841,6 @@ interaction type(s): |=== - - [[rsocket-annot-rsocketexchange]] === @RSocketExchange @@ -879,7 +855,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 +874,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- interface RadarsService { @@ -927,7 +903,6 @@ xref:rsocket-interface[RSocket Interface] for a list of supported parameters. `@RSocketExchange` can be used at the type level to specify a common prefix for all routes for a given RSocket service interface. - [[rsocket-annot-connectmapping]] === @ConnectMapping @@ -948,14 +923,12 @@ requests to the `RSocketRequester` for the connection. See xref:rsocket.adoc#rsocket-requester-server[Server Requester] for details. - - [[rsocket-metadata-extractor]] == MetadataExtractor Responders must interpret metadata. {rsocket-protocol-extensions}/CompositeMetadata.md[Composite metadata] allows independently -formatted metadata values (e.g. for routing, security, tracing) each with its own mime +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. @@ -973,7 +946,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 +954,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 +972,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 +985,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 +1004,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 +1016,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 @@ -1057,8 +1030,6 @@ Kotlin:: ====== - - [[rsocket-interface]] == RSocket Interface @@ -1095,8 +1066,6 @@ Now you can create a proxy that performs requests when methods are called: You can also implement the interface to handle requests as a responder. See xref:rsocket.adoc#rsocket-annot-rsocketexchange[Annotated Responders]. - - [[rsocket-interface-method-parameters]] === Method Parameters @@ -1115,7 +1084,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 @@ -1129,7 +1100,6 @@ method parameters: |=== - [[rsocket-interface-return-values]] === Return Values @@ -1142,4 +1112,3 @@ signature depends on response timeout settings of the underlying RSocket `Client as well as RSocket keep-alive settings. `RSocketServiceProxyFactory.Builder` does expose a `blockTimeout` option that also lets you configure the maximum time to block for a response, but we recommend configuring timeout values at the RSocket level for more control. - diff --git a/framework-docs/modules/ROOT/pages/testing.adoc b/framework-docs/modules/ROOT/pages/testing.adoc index ee17a98bb368..30664384c345 100644 --- a/framework-docs/modules/ROOT/pages/testing.adoc +++ b/framework-docs/modules/ROOT/pages/testing.adoc @@ -8,13 +8,3 @@ found that the correct use of inversion of control (IoC) certainly does make bot and integration testing easier (in that the presence of setter methods and appropriate constructors on classes makes them easier to wire together in a test without having to set up service locator registries and similar structures). - - - - - - - - - - diff --git a/framework-docs/modules/ROOT/pages/testing/annotations.adoc b/framework-docs/modules/ROOT/pages/testing/annotations.adoc index 844885ee0d2c..f90d084d4264 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations.adoc @@ -3,4 +3,3 @@ :page-section-summary-toc: 1 This section covers annotations that you can use when you test Spring applications. - 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 64af8240b803..820ce06455eb 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc @@ -2,9 +2,10 @@ = Spring JUnit Jupiter Testing Annotations The following annotations are supported when used in conjunction with the -xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] and JUnit Jupiter -(that is, the programming model in JUnit 5): +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and the JUnit Jupiter testing framework: +* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitconfig[`@SpringJUnitConfig`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitwebconfig[`@SpringJUnitWebConfig`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`] @@ -13,6 +14,57 @@ xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupite * 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-springextensionconfig]] +== `@SpringExtensionConfig` + +`@SpringExtensionConfig` is a type-level annotation that can be used to configure the +behavior of the `SpringExtension`. + +As of Spring Framework 7.0, the `SpringExtension` is configured to use a test-method +scoped `ExtensionContext`, which enables consistent dependency injection into fields and +constructors from the `ApplicationContext` for the current test method in a `@Nested` +test class hierarchy. However, if a third-party `TestExecutionListener` is not compatible +with the semantics associated with a test-method scoped extension context — or if a +developer wishes to switch to test-class scoped semantics — the `SpringExtension` can be +configured to use a test-class scoped `ExtensionContext` by annotating a top-level test +class with `@SpringExtensionConfig(useTestClassScopedExtensionContext = true)`. + +Alternatively, you can change the global default by setting the +`spring.test.extension.context.scope` property to `test_class`. The property is resolved +first via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism +which also supports JVM system properties — for example, +`-Dspring.test.extension.context.scope=test_class`. If the Spring property has not been +set, the `SpringExtension` will attempt to resolve the property as a +https://docs.junit.org/current/running-tests/configuration-parameters.html[JUnit Platform configuration parameter] +as a fallback mechanism. If the property has not been set via either of those mechanisms, +the `SpringExtension` will use a test-method scoped extension context by default. Note, +however, that a `@SpringExtensionConfig` declaration always takes precedence over this +property. + +[TIP] +==== +If a test class uses JUnit Jupiter's `@TestInstance(Lifecycle.PER_CLASS)` semantics, the +`SpringExtension` will always use a test-class scoped `ExtensionContext`, and +configuration via `@SpringExtensionConfig(useTestClassScopedExtensionContext = true)` or +the `spring.test.extension.context.scope` property will have no effect for that test +class. +==== + +[NOTE] +==== +This annotation is currently only applicable to `@Nested` test class hierarchies and +should be applied to the top-level enclosing class of a `@Nested` test class hierarchy. +Consequently, there is no need to declare this annotation on a test class that does not +contain `@Nested` test classes. + +In addition, +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[`@NestedTestConfiguration`] +does not apply to this annotation. `@SpringExtensionConfig` will always be detected +within a `@Nested` test class hierarchy, effectively disregarding any +`@NestedTestConfiguration(OVERRIDE)` declarations. +==== + [[integration-testing-annotations-junit-jupiter-springjunitconfig]] == `@SpringJUnitConfig` @@ -30,7 +82,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 { @@ -41,7 +93,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 { @@ -59,7 +111,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 { @@ -70,7 +122,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 { @@ -85,6 +137,7 @@ See xref:testing/testcontext-framework/ctx-management.adoc[Context Management] a {spring-framework-api}/test/context/junit/jupiter/SpringJUnitConfig.html[`@SpringJUnitConfig`] and `@ContextConfiguration` for further details. + [[integration-testing-annotations-junit-jupiter-springjunitwebconfig]] == `@SpringJUnitWebConfig` @@ -105,7 +158,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 { @@ -116,7 +169,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 { @@ -134,7 +187,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 { @@ -145,7 +198,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 { @@ -162,6 +215,7 @@ See xref:testing/testcontext-framework/ctx-management.adoc[Context Management] a {spring-framework-api}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] for further details. + [[integration-testing-annotations-testconstructor]] == `@TestConstructor` @@ -171,9 +225,9 @@ the parameters of a test class constructor are autowired from components in the If `@TestConstructor` is not present or meta-present on a test class, the default _test constructor autowire mode_ will be used. See the tip below for details on how to change -the default mode. Note, however, that a local declaration of `@Autowired`, -`@jakarta.inject.Inject`, or `@javax.inject.Inject` on a constructor takes precedence -over both `@TestConstructor` and the default mode. +the default mode. Note, however, that a local declaration of `@Autowired` or +`@jakarta.inject.Inject` on a constructor takes precedence over both `@TestConstructor` +and the default mode. .Changing the default test constructor autowire mode [TIP] @@ -183,18 +237,18 @@ 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 -https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter]. +The default mode may also be configured as a +https://docs.junit.org/current/running-tests/configuration-parameters.html[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` @@ -244,8 +298,9 @@ with `@Nested` test classes in JUnit Jupiter; however, there may be other testin frameworks with support for Spring and nested test classes that make use of this annotation. -See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] for an example and further -details. +See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] +for an example and further details. + [[integration-testing-annotations-junit-jupiter-enabledif]] == `@EnabledIf` @@ -275,7 +330,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) @@ -288,7 +343,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) @@ -313,6 +368,7 @@ if you wish to use Spring's `@EnabledIf` support make sure you import the annota from the correct package. ==== + [[integration-testing-annotations-junit-jupiter-disabledif]] == `@DisabledIf` @@ -341,7 +397,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) @@ -354,7 +410,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) @@ -378,6 +434,3 @@ Since JUnit 5.7, JUnit Jupiter also has a condition annotation named `@DisabledI if you wish to use Spring's `@DisabledIf` support make sure you import the annotation type from the correct package. ==== - - - 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 3ac9413ff6d3..9cd0fabdf4ee 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc @@ -1,15 +1,24 @@ [[integration-testing-annotations-junit4]] = Spring JUnit 4 Testing Annotations +[WARNING] +==== +JUnit 4 support is deprecated since Spring Framework 7.0 in favor of the +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and JUnit Jupiter. +==== + The following annotations are supported only when used in conjunction with the -xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner], xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules] -, or xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]: +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner], +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules], or +xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]: * xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-ifprofilevalue[`@IfProfileValue`] * xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-profilevaluesourceconfiguration[`@ProfileValueSourceConfiguration`] * xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-timed[`@Timed`] * xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-repeat[`@Repeat`] + [[integration-testing-annotations-junit4-ifprofilevalue]] == `@IfProfileValue` @@ -31,7 +40,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 +52,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 +72,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 +84,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 @@ -101,7 +110,7 @@ is used by default. The following example shows how to use ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @ProfileValueSourceConfiguration(CustomProfileValueSource.class) // <1> public class CustomProfileValueSourceTests { @@ -112,7 +121,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 { @@ -138,7 +147,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() { @@ -149,7 +158,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() { @@ -167,6 +176,7 @@ preemptively fails the test if the test takes too long. Spring's `@Timed`, on th hand, does not preemptively fail the test but rather waits for the test to complete before failing. + [[integration-testing-annotations-junit4-repeat]] == `@Repeat` @@ -175,15 +185,15 @@ times that the test method is to be run is specified in the annotation. The scope of execution to be repeated includes execution of the test method itself as well as any setting up or tearing down of the test fixture. When used with the -xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[`SpringMethodRule`], the scope additionally includes -preparation of the test instance by `TestExecutionListener` implementations. The -following example shows how to use the `@Repeat` annotation: +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[`SpringMethodRule`], +the scope additionally includes preparation of the test instance by `TestExecutionListener` +implementations. The following example shows how to use the `@Repeat` annotation: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Repeat(10) // <1> @Test @@ -195,7 +205,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Repeat(10) // <1> @Test @@ -205,6 +215,3 @@ Kotlin:: ---- <1> Repeat this test ten times. ====== - - - diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc index 78164ab07744..7c30b5b6f11e 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-meta.adoc @@ -2,11 +2,12 @@ = Meta-Annotation Support for Testing You can use most test-related annotations as -xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] to create custom composed -annotations and reduce configuration duplication across a test suite. +xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] to +create custom composed annotations and reduce configuration duplication across a test +suite. -You can use each of the following as a meta-annotation in conjunction with the -xref:testing/testcontext-framework.adoc[TestContext framework]. +For example, you can use each of the following as a meta-annotation in conjunction with +the xref:testing/testcontext-framework.adoc[TestContext framework]. * `@BootstrapWith` * `@ContextConfiguration` @@ -37,126 +38,22 @@ xref:testing/testcontext-framework.adoc[TestContext framework]. * `@EnabledIf` _(only supported on JUnit Jupiter)_ * `@DisabledIf` _(only supported on JUnit Jupiter)_ -Consider the following example: +Consider the following test classes that use the `SpringExtension` with JUnit Jupiter: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @RunWith(SpringRunner.class) - @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) - @ActiveProfiles("dev") - @Transactional - public class OrderRepositoryTests { } - - @RunWith(SpringRunner.class) - @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) - @ActiveProfiles("dev") - @Transactional - public class UserRepositoryTests { } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @RunWith(SpringRunner::class) - @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") - @ActiveProfiles("dev") - @Transactional - class OrderRepositoryTests { } - - @RunWith(SpringRunner::class) - @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") - @ActiveProfiles("dev") - @Transactional - class UserRepositoryTests { } ----- -====== - -If we discover that we are repeating the preceding configuration across our JUnit 4-based -test suite, we can reduce the duplication by introducing a custom composed annotation -that centralizes the common test configuration for Spring, as follows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Target(ElementType.TYPE) - @Retention(RetentionPolicy.RUNTIME) - @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) - @ActiveProfiles("dev") - @Transactional - public @interface TransactionalDevTestConfig { } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Target(AnnotationTarget.TYPE) - @Retention(AnnotationRetention.RUNTIME) - @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") - @ActiveProfiles("dev") - @Transactional - annotation class TransactionalDevTestConfig { } ----- -====== - -Then we can use our custom `@TransactionalDevTestConfig` annotation to simplify the -configuration of individual JUnit 4 based test classes, as follows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @RunWith(SpringRunner.class) - @TransactionalDevTestConfig - public class OrderRepositoryTests { } - - @RunWith(SpringRunner.class) - @TransactionalDevTestConfig - public class UserRepositoryTests { } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @RunWith(SpringRunner::class) - @TransactionalDevTestConfig - class OrderRepositoryTests - - @RunWith(SpringRunner::class) - @TransactionalDevTestConfig - class UserRepositoryTests ----- -====== - -If we write tests that use JUnit Jupiter, we can reduce code duplication even further, -since annotations in JUnit 5 can also be used as meta-annotations. Consider the following -example: - -[tabs] -====== -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"}) + @ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class}) @ActiveProfiles("dev") @Transactional class OrderRepositoryTests { } @ExtendWith(SpringExtension.class) - @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) + @ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class}) @ActiveProfiles("dev") @Transactional class UserRepositoryTests { } @@ -164,37 +61,36 @@ 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") + @ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class]) @ActiveProfiles("dev") @Transactional class OrderRepositoryTests { } @ExtendWith(SpringExtension::class) - @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") + @ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class]) @ActiveProfiles("dev") @Transactional class UserRepositoryTests { } ---- ====== -If we discover that we are repeating the preceding configuration across our JUnit -Jupiter-based test suite, we can reduce the duplication by introducing a custom composed -annotation that centralizes the common test configuration for Spring and JUnit Jupiter, -as follows: +If we discover that we are repeating the preceding configuration across our test suite, +we can reduce the duplication by introducing a custom composed annotation that +centralizes the common test configuration for Spring and JUnit Jupiter, as follows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(SpringExtension.class) - @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) + @ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class}) @ActiveProfiles("dev") @Transactional public @interface TransactionalDevTestConfig { } @@ -202,12 +98,12 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @ExtendWith(SpringExtension::class) - @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") + @ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class]) @ActiveProfiles("dev") @Transactional annotation class TransactionalDevTestConfig { } @@ -221,7 +117,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 +128,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @TransactionalDevTestConfig class OrderRepositoryTests { } @@ -253,7 +149,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 +161,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 +179,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 +190,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @TransactionalIntegrationTest fun saveOrder() { } 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 300c32dc8911..8c500e124b5d 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc @@ -30,4 +30,3 @@ Spring's testing annotations include the following: * 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 4d41be0bf683..43da00956a6d 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 @@ -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> @@ -72,8 +72,14 @@ bean definition profiles programmatically by implementing a custom xref:testing/testcontext-framework/ctx-management/env-profiles.adoc#testcontext-ctx-management-env-profiles-ActiveProfilesResolver[`ActiveProfilesResolver`] and registering it by using the `resolver` attribute of `@ActiveProfiles`. +NOTE: When `@ActiveProfiles` is declared on a test class, the `spring.profiles.active` +property (whether configured as a JVM system property or environment variable) is not +taken into account by the TestContext Framework when determining active profiles. If +you need to allow `spring.profiles.active` to override the profiles configured via +`@ActiveProfiles`, you can implement a custom `ActiveProfilesResolver` as described in +xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[Context Configuration with Environment Profiles]. + 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 {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..def7d93b425d 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 @@ -4,14 +4,13 @@ `@AfterTransaction` indicates that the annotated `void` method should be run after a transaction is ended, for test methods that have been configured to run within a transaction by using Spring's `@Transactional` annotation. `@AfterTransaction` methods -are not required to be `public` and may be declared on Java 8-based interface default -methods. +are not required to be `public` and may be declared on interface default methods. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @AfterTransaction // <1> void afterTransaction() { @@ -22,7 +21,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @AfterTransaction // <1> fun afterTransaction() { @@ -31,5 +30,3 @@ Kotlin:: ---- <1> Run this method after a transaction. ====== - - 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..b1cf61f886f6 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 @@ -4,8 +4,7 @@ `@BeforeTransaction` indicates that the annotated `void` method should be run before a transaction is started, for test methods that have been configured to run within a transaction by using Spring's `@Transactional` annotation. `@BeforeTransaction` methods -are not required to be `public` and may be declared on Java 8-based interface default -methods. +are not required to be `public` and may be declared on interface default methods. The following example shows how to use the `@BeforeTransaction` annotation: @@ -13,7 +12,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 +23,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @BeforeTransaction // <1> fun beforeTransaction() { @@ -33,5 +32,3 @@ Kotlin:: ---- <1> Run this method before a transaction. ====== - - 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 a297b0149944..65bf7e83777b 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 @@ -6,4 +6,3 @@ 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..d1e956677c81 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 @@ -35,5 +35,3 @@ Kotlin:: ---- <1> Commit the result of the test to the database. ====== - - 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 55eb8169f0a2..f00b4db40d8b 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 @@ -10,7 +10,8 @@ Resource locations are typically XML configuration files or Groovy scripts locat classpath, while component classes are typically `@Configuration` classes. However, resource locations can also refer to files and scripts in the file system, and component classes can be `@Component` classes, `@Service` classes, and so on. See -xref:testing/testcontext-framework/ctx-management/javaconfig.adoc#testcontext-ctx-management-javaconfig-component-classes[Component Classes] for further details. +xref:testing/testcontext-framework/ctx-management/javaconfig.adoc#testcontext-ctx-management-javaconfig-component-classes[Component Classes] +for further details. The following example shows a `@ContextConfiguration` annotation that refers to an XML file: @@ -19,7 +20,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 +31,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 +48,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 +59,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 +78,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 +89,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 +111,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 +122,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 { @@ -137,6 +138,6 @@ configuration classes as well as context initializers that are declared by super or enclosing classes. See xref:testing/testcontext-framework/ctx-management.adoc[Context Management], -xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration], and the `@ContextConfiguration` +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration], +and the {spring-framework-api}/test/context/ContextConfiguration.html[`@ContextConfiguration`] javadocs for further details. - 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 3eb88e7ea0d2..758b36f7809c 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 @@ -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> @@ -39,7 +39,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 -{spring-framework-api}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories` -javadoc] for an example and further details. - +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] +and the {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 e1fd02d1168d..552520d81002 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 @@ -12,7 +12,7 @@ used within a test class hierarchy): ====== 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( @@ -69,7 +69,6 @@ Kotlin:: If you need to merge or override the configuration for a given level of the context 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 -{spring-framework-api}/test/context/ContextHierarchy.html[`@ContextHierarchy`] javadoc +level in the class hierarchy. See xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[Context Hierarchies] +and the {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 75d5856ad481..3a2c8a2ed2c4 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 @@ -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"), @@ -259,4 +259,3 @@ Kotlin:: For further details regarding the `EXHAUSTIVE` and `CURRENT_LEVEL` algorithms, see the {spring-framework-api}/test/annotation/DirtiesContext.HierarchyMode.html[`DirtiesContext.HierarchyMode`] javadoc. - 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 45457a62cd08..ed0705d21dad 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 @@ -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 { @@ -60,5 +60,5 @@ Kotlin:: <3> Register a dynamic `server.port` property to be retrieved lazily from the server. ====== -See xref:testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc[Context Configuration with Dynamic Property Sources] for further details. - +See xref:testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc[Context Configuration with Dynamic Property Sources] +for further details. diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc index d6de89bd344e..8191b9cf99dd 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc @@ -1,51 +1,913 @@ [[spring-testing-annotation-beanoverriding-mockitobean]] = `@MockitoBean` and `@MockitoSpyBean` -`@MockitoBean` and `@MockitoSpyBean` are used on test class fields to override beans in -the test's `ApplicationContext` with a Mockito mock or spy, respectively. In the latter -case, the original bean definition is not replaced, but instead an early instance of the -bean is captured and wrapped by the spy. +{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. -By default, the annotated field's type is used to search for candidate definitions to -override, but note that `@Qualifier` annotations are also taken into account for the -purpose of matching. Users can also make things entirely explicit by specifying a bean -`name` in the annotation. +The annotations can be applied in the following ways. -Each annotation also defines Mockito-specific attributes to fine-tune the mocking details. +* 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. +* On a parameter in the constructor for a 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. -The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE_DEFINITION` -xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding]. +When `@MockitoBean` or `@MockitoSpyBean` is declared on a field or constructor parameter, +the bean to mock or spy is inferred from the type of the annotated field or parameter. If +multiple candidates exist in the `ApplicationContext`, a `@Qualifier` annotation can be +declared on the field or parameter to help disambiguate. In the absence of a `@Qualifier` +annotation, the name of the annotated field or parameter 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. -It requires that at most one matching candidate definition exists if a bean name -is specified, or exactly one if no bean name is specified. +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)`. -The `@MockitoSpyBean` annotation uses the `WRAP_BEAN` -xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy], -and the original instance is wrapped in a Mockito spy. +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. -It requires that exactly one candidate definition exists. +[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. +==== -The following example shows how to configure the bean name via `@MockitoBean` and -`@MockitoSpyBean`: +[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)`. Because this strategy replaces the +bean directly, bypassing the container's normal bean post-processing, the resulting mock +is a bare object: it is never wrapped in a Spring AOP proxy, even if the original bean +would have been — for example, due to `@Transactional`, `@Cacheable`, or `@Retryable`. See +xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-aop-proxies[Bean +Overrides and Spring AOP Proxies] for details. + +The `@MockitoSpyBean` annotation uses the `WRAP` +xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy]: +an early instance of the original bean is captured and used to create a Mockito spy. +This strategy requires that exactly one candidate bean exists. In contrast to +`@MockitoBean`, if the original bean would have been wrapped in a Spring AOP proxy, that +proxy is still created — but it now wraps the spy instead of the original bean. See +<> +for a diagram and further details on the consequences this has for stubbing and +verification. + +[TIP] +==== +As stated in the documentation for Mockito, there are times when using `Mockito.when()` is +inappropriate for stubbing a spy – for example, if calling a real method on a spy results +in undesired side effects. + +To avoid such undesired side effects, consider using +`Mockito.doReturn(...).when(spy)...`, `Mockito.doThrow(...).when(spy)...`, +`Mockito.doNothing().when(spy)...`, and similar methods. +==== + +[NOTE] +==== +When using `@MockitoBean` to mock a non-singleton bean, the non-singleton bean will be +replaced with a singleton mock, and the corresponding bean definition will be converted +to a `singleton`. Consequently, if you mock a `prototype` or scoped bean, the mock will +be treated as a `singleton`. + +Similarly, when using `@MockitoSpyBean` to create a spy for a non-singleton bean, the +corresponding bean definition will be converted to a `singleton`. Consequently, if you +create a spy for a `prototype` or scoped bean, the spy will be treated as a `singleton`. + +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`. + +Similarly, 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. + +Furthermore, `@MockitoSpyBean` cannot be used to spy on a scoped proxy — for example, a +bean annotated with `@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)`. Any attempt to do +so will fail with an exception. +==== + +[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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests { + + @MockitoBean // <1> + lateinit var 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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests { + + @MockitoBean("service") // <1> + lateinit var customService: CustomService + + // tests... + + } +---- +<1> Replace the bean named `service` with a Mockito mock. +====== + +The following example shows how to use `@MockitoBean` on a constructor parameter for a +by-type lookup. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + private final CustomService customService; + + BeanOverrideTests(@MockitoBean CustomService customService) { // <1> + this.customService = customService; + } + + // tests... + } +---- +<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into + the constructor. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests(@MockitoBean val customService: CustomService) { // <1> + + // tests... + } +---- +<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into + the constructor. +====== + +The following example shows how to use `@MockitoBean` on a constructor parameter for a +by-name lookup. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + private final CustomService customService; + + BeanOverrideTests(@MockitoBean("service") CustomService customService) { // <1> + this.customService = customService; + } + + // tests... + } +---- +<1> Replace the bean named `service` with a Mockito mock and inject it into the + constructor. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - class OverrideBeanTests { + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests(@MockitoBean("service") val customService: CustomService) { // <1> + + // tests... + } +---- +<1> Replace the bean named `service` with a Mockito mock and inject it into the + constructor. +====== - @MockitoBean(name = "service1") // <1> - private CustomService mockService; +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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Target(AnnotationTarget.CLASS) + @Retention(AnnotationRetention.RUNTIME) + @MockitoBean(types = [OrderService::class, UserService::class]) // <1> + @MockitoBean(name = "ps1", types = [PrintingService::class]) // <2> + annotation class 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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + @SharedMocks // <1> + class BeanOverrideTests { + + @Autowired + lateinit var orderService: OrderService // <2> + + @Autowired + lateinit var userService: UserService // <2> + + @Autowired + lateinit var ps1: PrintingService // <2> + + // Inject other components that rely on the mocks. + + @Test + fun 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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests { + + @MockitoSpyBean // <1> + lateinit var 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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests { + + @MockitoSpyBean("service") // <1> + lateinit var customService: CustomService + + // tests... + } +---- +<1> Wrap the bean named `service` with a Mockito spy. +====== + +The following example shows how to use `@MockitoSpyBean` on a constructor parameter for +a by-type lookup. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { - @MockitoSpyBean(name = "service2") // <2> - private CustomService spyService; // <3> + private final CustomService customService; + + BeanOverrideTests(@MockitoSpyBean CustomService customService) { // <1> + this.customService = customService; + } + + // tests... + } +---- +<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the + constructor. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests(@MockitoSpyBean val customService: CustomService) { // <1> + + // tests... + } +---- +<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the + constructor. +====== + +The following example shows how to use `@MockitoSpyBean` on a constructor parameter for +a by-name lookup. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class BeanOverrideTests { + + private final CustomService customService; + + BeanOverrideTests(@MockitoSpyBean("service") CustomService customService) { // <1> + this.customService = customService; + } + + // tests... + } +---- +<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class BeanOverrideTests(@MockitoSpyBean("service") val customService: CustomService) { // <1> + + // tests... + } +---- +<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor. +====== + +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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Target(AnnotationTarget.CLASS) + @Retention(AnnotationRetention.RUNTIME) + @MockitoSpyBean(types = [OrderService::class, UserService::class]) // <1> + @MockitoSpyBean(name = "ps1", types = [PrintingService::class]) // <2> + annotation class 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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + @SharedSpies // <1> + class BeanOverrideTests { + + @Autowired + lateinit var orderService: OrderService // <2> + + @Autowired + lateinit var userService: UserService // <2> + + @Autowired + lateinit var ps1: PrintingService // <2> + + // Inject other components that rely on the spies. + + @Test + fun 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. + + +[[spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies]] +== `@MockitoSpyBean` and Spring AOP Proxies + +As explained in +xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-aop-proxies[Bean +Overrides and Spring AOP Proxies], if the bean being spied on would normally be wrapped in +a Spring AOP proxy — for example, due to `@Transactional`, `@Cacheable`, or `@Retryable` +— that proxy is still created, with the spy as its target. The bean injected into the +test class and into other beans in the `ApplicationContext` is therefore the proxy, not +the spy itself. + +Verification via Mockito's `verify()` API is unaffected by this and works transparently, +regardless of whether it is invoked on the proxy or on the underlying spy. + +[[spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies-stubbing]] +=== Stubbing Through the Proxy + +Stubbing requires more care than verification, since `Mockito.doReturn(...).when(...)`, +`Mockito.doThrow(...).when(...)`, and similar methods behave differently depending on the +nature of the AOP advice involved when invoked on the proxy. + +NOTE: Since `when` is a reserved keyword in Kotlin, the Kotlin examples below use the +`given(...)`, `willReturn(...)`, and `willThrow(...)` methods from `BDDMockito` instead +of `Mockito.doReturn(...).when(...)` and `Mockito.doThrow(...).when(...)`. + +Advice that does not retain state between invocations — such as +xref:core/resilience.adoc#resilience-annotations-retryable[`@Retryable`] — has no adverse +effect on stubbing. The following stubbing sequence, invoked on the proxy, behaves exactly +as it would on the underlying spy directly, including triggering a retry when the thrown +exception is encountered. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + doReturn("ok") + .doThrow(new RuntimeException("Message delivery failed")) + .doReturn("ok again") + .when(clientService).sendMessage(any()); // <1> +---- +<1> `clientService` is the injected proxy. Since `@Retryable` advice is a stateless + pass-through, each call — including the one that throws — reaches the spy directly. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + willReturn("ok") + .willThrow(RuntimeException("Message delivery failed")) + .willReturn("ok again") + .given(clientService).sendMessage(any()) // <1> +---- +<1> `clientService` is the injected proxy. Since `@Retryable` advice is a stateless + pass-through, each call — including the one that throws — reaches the spy directly. +====== + +Advice that caches or otherwise memoizes the outcome of an invocation — such as +`@Cacheable` — does not behave the same way. While a `doReturn(...)`, `doThrow(...)`, or +similar declaration is being recorded, Mockito does not invoke the spy's real or +previously stubbed behavior; instead, the invocation used to declare the stubbing returns +an empty value (for example, `null`). If that invocation is made on the proxy, the caching +advice caches this empty value, which then permanently shadows the spy for that +combination of arguments — including for the very invocation that was supposed to +configure the stubbing. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + doReturn(1L).when(dateService).getDate(false); // <1> + dateService.getDate(false); // <2> +---- +<1> `dateService` is the injected proxy. This invocation is intercepted by Mockito's + stubbing infrastructure before it reaches the spy, so the caching advice ends up + caching an empty value for argument `false`. +<2> Returns the empty value cached by the previous invocation — not `1L` — because the + cache was already populated. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + willReturn(1L).given(dateService).getDate(false) // <1> + dateService.getDate(false) // <2> +---- +<1> `dateService` is the injected proxy. This invocation is intercepted by Mockito's + stubbing infrastructure before it reaches the spy, so the caching advice ends up + caching an empty value for argument `false`. +<2> Returns the empty value cached by the previous invocation — not `1L` — because the + cache was already populated. +====== + +To avoid this, stub directly on the spy instead of on the proxy, by unwrapping the proxy +with +{spring-framework-api}/test/util/AopTestUtils.html#getUltimateTargetObject(java.lang.Object)[`AopTestUtils.getUltimateTargetObject(...)`]. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + DateService spy = AopTestUtils.getUltimateTargetObject(dateService); + doReturn(1L).when(spy).getDate(false); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val spy = AopTestUtils.getUltimateTargetObject(dateService) + willReturn(1L).given(spy).getDate(false) +---- +====== + +[[spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies-disabling]] +=== Disabling AOP Advice for Tests + +Rather than working around the proxy as shown above, you may instead prefer to disable +the underlying AOP advice for the duration of the test, while keeping `@Retryable`, +`@Cacheable`, or similar annotations in place in production code. Common reasons include +avoiding retry delays that slow down the test suite, or avoiding caching altogether so +that every invocation reaches the spy directly — which also sidesteps the stubbing +pitfall described above, without having to unwrap the proxy at all. + +The general technique is to externalize whatever controls the advice's effective behavior +— for example, the number of retry attempts or the `CacheManager` backing `@Cacheable` +— and override that configuration for tests only, typically by using a bean override or a +test-specific property. The proxy and its advice are still created, but their behavior is +simply made a no-op or pure pass-through for the test. + +For `@Retryable`, bind the `maxRetriesString` attribute to a property placeholder with a +sensible default (so that production configuration is unaffected if the property is not +set), and override that property in the test with +xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`] +so that no retries are attempted. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Retryable(maxRetriesString = "${sendMessage.maxRetries:3}", delay = 10) + public String sendMessage(String request) { + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Retryable(maxRetriesString = "\${sendMessage.maxRetries:3}", delay = 10) + fun sendMessage(request: String): String { + // ... + } +---- +====== + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig + @TestPropertySource(properties = "sendMessage.maxRetries = 0") // <1> + class ClientServiceTests { + + @MockitoSpyBean + ClientService clientService; + + // test case body... + } +---- +<1> With no retries permitted, the first (and only) attempt is made, and a thrown + exception propagates immediately, so the spy's stubbing chain behaves exactly as + declared, including for `doThrow(...)` answers. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig + @TestPropertySource(properties = ["sendMessage.maxRetries = 0"]) // <1> + class ClientServiceTests { + + @MockitoSpyBean + lateinit var clientService: ClientService // test case body... } ---- -<1> Mark `mockService` as a Mockito mock override of bean `service1` in this test class. -<2> Mark `spyService` as a Mockito spy override of bean `service2` in this test class. -<3> The fields will be injected with the Mockito mock and spy, respectively. +<1> With no retries permitted, the first (and only) attempt is made, and a thrown + exception propagates immediately, so the spy's stubbing chain behaves exactly as + declared, including for `doThrow(...)` answers. +====== + +For `@Cacheable`, Spring provides +{spring-framework-api}/cache/support/NoOpCacheManager.html[`NoOpCacheManager`] — a +`CacheManager` that accepts cache entries but never actually stores them, so every +invocation results in a cache miss and therefore an invocation of the target method. +Overriding the `CacheManager` bean with a `NoOpCacheManager` — for example, with +xref:testing/annotations/integration-spring/annotation-testbean.adoc[`@TestBean`] — +effectively disables caching for the test without touching the `@Cacheable` annotation in +production code. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig + class DateServiceTests { + + @MockitoSpyBean + DateService dateService; + + @TestBean // <1> + CacheManager cacheManager; + + static CacheManager cacheManager() { // <2> + return new NoOpCacheManager(); + } + + @Test + void test() { + doReturn(1L).when(dateService).getDate(false); + assertThat(dateService.getDate(false)).isEqualTo(1L); + + doReturn(2L).when(dateService).getDate(false); + assertThat(dateService.getDate(false)).isEqualTo(2L); // <3> + } + } +---- +<1> Override the `CacheManager` bean for this test. +<2> Replace it with a `NoOpCacheManager`, so `@Cacheable` never actually caches anything. +<3> No longer masked by a stale cache entry, since every call reaches the spy. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig + class DateServiceTests { + + @MockitoSpyBean + lateinit var dateService: DateService + + @TestBean // <1> + lateinit var cacheManager: CacheManager + + companion object { + @JvmStatic + fun cacheManager(): CacheManager { // <2> + return NoOpCacheManager() + } + } + + @Test + fun test() { + willReturn(1L).given(dateService).getDate(false) + assertThat(dateService.getDate(false)).isEqualTo(1L) + + willReturn(2L).given(dateService).getDate(false) + assertThat(dateService.getDate(false)).isEqualTo(2L) // <3> + } + } +---- +<1> Override the `CacheManager` bean for this test. +<2> Replace it with a `NoOpCacheManager`, so `@Cacheable` never actually caches anything. +<3> No longer masked by a stale cache entry, since every call reaches the spy. ====== 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 15f41b4192bb..5a37f56de508 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 @@ -11,4 +11,3 @@ The recorded events can be accessed via the `ApplicationEvents` API within tests See xref:testing/testcontext-framework/application-events.adoc[Application Events] and the {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..bf4ef2a8a45d 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 @@ -41,5 +41,3 @@ Kotlin:: ---- <1> Do not roll back the result. ====== - - 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..17c4ec885a26 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> @@ -32,6 +32,5 @@ Kotlin:: <1> Run two scripts for this test. ====== -See xref:testing/testcontext-framework/executing-sql.adoc#testcontext-executing-sql-declaratively[Executing SQL scripts declaratively with @Sql] for further details. - - +See xref:testing/testcontext-framework/executing-sql.adoc#testcontext-executing-sql-declaratively[Executing SQL scripts declaratively with @Sql] +for further details. 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..114aa7e6dbaa 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> @@ -33,4 +33,3 @@ Kotlin:: ---- <1> Set the comment prefix and the separator in SQL scripts. ====== - 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..7755169e01b6 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 @@ -3,7 +3,7 @@ `@SqlGroup` is a container annotation that aggregates several `@Sql` annotations. You can use `@SqlGroup` natively to declare several nested `@Sql` annotations, or you can use it -in conjunction with Java 8's support for repeatable annotations, where `@Sql` can be +in conjunction with Java's support for repeatable annotations, where `@Sql` can be declared several times on the same class or method, implicitly generating this container annotation. The following example shows how to declare an SQL group: @@ -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> @@ -38,6 +38,3 @@ Kotlin:: ---- <1> Declare a group of SQL scripts. ====== - - - 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..36c5bb8c28e7 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") @@ -90,5 +90,3 @@ Kotlin:: ---- <1> Set the `@Sql` merge mode to `MERGE` for a specific test method. ====== - - diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc index 3da3da2a4fce..b42548ce6dae 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc @@ -1,44 +1,185 @@ [[spring-testing-annotation-beanoverriding-testbean]] = `@TestBean` -`@TestBean` is used on a test class field to override a specific bean in the test's -`ApplicationContext` with an instance provided by a conventionally named static factory -method. +{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. -By default, the associated factory method name is derived from the annotated field's name, -but the annotation allows for a specific method name to be provided. +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. -The `@TestBean` annotation uses the `REPLACE_DEFINITION` -xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding]. +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. -By default, the annotated field's type is used to search for candidate definitions to override. -In that case it is required that exactly one definition matches, but note that `@Qualifier` -annotations are also taken into account for the purpose of matching. -Users can also make things entirely explicit by specifying a bean `name` in the annotation. +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)`. -The following example shows how to fully configure the `@TestBean` annotation, with -explicit values equivalent to the defaults: +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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + class OverrideBeanTests { + @TestBean // <1> + lateinit var customService: CustomService + + // test case body... + + companion object { + @JvmStatic + fun customService(): CustomService { // <2> + return 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",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class OverrideBeanTests { - @TestBean(name = "service", methodName = "serviceTestOverride") // <1> - private CustomService service; + @TestBean(name = "service", methodName = "createCustomService") // <1> + CustomService customService; // test case body... - private static CustomService serviceTestOverride() { // <2> + static CustomService createCustomService() { // <2> return new MyFakeCustomService(); } } ---- -<1> Mark a field for bean overriding in this test class. +<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. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + class OverrideBeanTests { + @TestBean(name = "service", methodName = "createCustomService") // <1> + lateinit var customService: CustomService + + // test case body... + + companion object { + @JvmStatic + fun createCustomService(): CustomService { // <2> + return 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. ====== -NOTE: Spring searches for the factory method to invoke in the test class, in the test -class hierarchy, and in the enclosing class hierarchy for a `@Nested` test class. +[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"`. +==== + +[NOTE] +==== +When overriding a non-singleton bean, the non-singleton bean will be replaced with a +singleton bean corresponding to the value returned from the `@TestBean` factory method, +and the corresponding bean definition will be converted to a `singleton`. Consequently, +if `@TestBean` is used to override a `prototype` or scoped bean, the overridden bean will +be treated as a `singleton`. + +Similarly, 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. +==== + +[NOTE] +==== +`@TestBean` uses the `REPLACE` or `REPLACE_OR_CREATE` +xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy +for bean overrides], which registers the value returned from the factory method directly +as the bean, bypassing the container's normal bean post-processing. Consequently, none of +the Spring AOP advice that would otherwise apply to the original bean (for example, +`@Transactional`, `@Cacheable`, or `@Retryable`) is present on the override instance. See +xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-aop-proxies[Bean +Overrides and Spring AOP Proxies] for details. +==== 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 2128f8b2c1e2..33cb39c5c8fc 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 @@ -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> @@ -38,9 +38,8 @@ 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 -{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]. - +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] +and the {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 e20851d78a22..bfba63c0a753 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 @@ -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> @@ -65,5 +65,5 @@ Kotlin:: <1> Declare `timezone` and `port` properties. ====== -See xref:testing/testcontext-framework/ctx-management/property-sources.adoc[Context Configuration with Test Property Sources] for examples and further details. - +See xref:testing/testcontext-framework/ctx-management/property-sources.adoc[Context Configuration with Test Property Sources] +for examples and further details. 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 b48e537fe709..fa697e62f9cc 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 @@ -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> @@ -41,7 +41,6 @@ Kotlin:: ====== -- - To override the default, you can specify a different base resource path by using the implicit `value` attribute. Both `classpath:` and `file:` resource prefixes are supported. If no resource prefix is supplied, the path is assumed to be a file system @@ -52,7 +51,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 +63,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> @@ -82,4 +81,3 @@ Note that `@WebAppConfiguration` must be used in conjunction with hierarchy. See the {spring-framework-api}/test/context/web/WebAppConfiguration.html[`@WebAppConfiguration`] javadoc for further details. - diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-standard.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-standard.adoc index 4406377286df..a25251a988c1 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-standard.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-standard.adoc @@ -9,7 +9,6 @@ and can be used anywhere in the Spring Framework. * `@Qualifier` * `@Value` * `@Resource` (jakarta.annotation) if JSR-250 is present -* `@ManagedBean` (jakarta.annotation) if JSR-250 is present * `@Inject` (jakarta.inject) if JSR-330 is present * `@Named` (jakarta.inject) if JSR-330 is present * `@PersistenceContext` (jakarta.persistence) if JPA is present @@ -32,6 +31,3 @@ the test class. On the other hand, if a method within a test class is annotated you use test lifecycle callbacks from the underlying test framework instead of `@PostConstruct` and `@PreDestroy`. ==== - - - diff --git a/framework-docs/modules/ROOT/pages/testing/appendix.adoc b/framework-docs/modules/ROOT/pages/testing/appendix.adoc index 0f1591ce97ad..404036a7acce 100644 --- a/framework-docs/modules/ROOT/pages/testing/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/testing/appendix.adoc @@ -1,5 +1,3 @@ [[appendix]] = Appendix :page-section-summary-toc: 1 - - diff --git a/framework-docs/modules/ROOT/pages/testing/integration.adoc b/framework-docs/modules/ROOT/pages/testing/integration.adoc index a7a4b39729ea..98b533838ed3 100644 --- a/framework-docs/modules/ROOT/pages/testing/integration.adoc +++ b/framework-docs/modules/ROOT/pages/testing/integration.adoc @@ -5,24 +5,25 @@ It is important to be able to perform some integration testing without requiring deployment to your application server or connecting to other enterprise infrastructure. Doing so lets you test things such as: -* The correct wiring of your Spring IoC container contexts. -* Data access using JDBC or an ORM tool. This can include such things as the correctness - of SQL statements, Hibernate queries, JPA entity mappings, and so forth. +* The correct wiring of your Spring components. +* Data access using JDBC or an ORM tool. + ** This can include such things as the correctness of SQL statements, Hibernate queries, + JPA entity mappings, and so forth. The Spring Framework provides first-class support for integration testing in the -`spring-test` module. The name of the actual JAR file might include the release version -and might also be in the long `org.springframework.test` form, depending on where you get -it from (see the xref:core/beans/dependencies.adoc[section on Dependency Management] -for an explanation). This library includes the `org.springframework.test` package, which +`spring-test` module. The name of the actual JAR file might include the release version, +depending on where you get it from (see the +{spring-framework-wiki}/Spring-Framework-Artifacts[Spring Framework Artifacts] wiki page +for details). This library includes the `org.springframework.test` package, which contains valuable classes for integration testing with a Spring container. This testing does not rely on an application server or other deployment environment. Such tests are slower to run than unit tests but much faster than the equivalent Selenium tests or remote tests that rely on deployment to an application server. Unit and integration testing support is provided in the form of the annotation-driven -xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext framework is -agnostic of the actual testing framework in use, which allows instrumentation of tests -in various environments, including JUnit, TestNG, and others. +xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext +framework is agnostic of the actual testing framework in use, which allows +instrumentation of tests in various environments, including JUnit, TestNG, and others. The following section provides an overview of the high-level goals of Spring's integration support, and the rest of this chapter then focuses on dedicated topics: @@ -30,12 +31,11 @@ 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] - [[integration-testing-goals]] == Goals of Integration Testing @@ -50,7 +50,6 @@ Spring's integration testing support has the following primary goals: The next few sections describe each goal and provide links to implementation and configuration details. - [[testing-ctx-management]] === Context Management and Caching @@ -78,10 +77,10 @@ reloading (for example, by modifying a bean definition or the state of an applic object) the TestContext framework can be configured to reload the configuration and rebuild the application context before executing the next test. -See xref:testing/testcontext-framework/ctx-management.adoc[Context Management] and xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] with the +See xref:testing/testcontext-framework/ctx-management.adoc[Context Management] and +xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] with the TestContext framework. - [[testing-fixture-di]] === Dependency Injection of Test Fixtures @@ -107,7 +106,6 @@ integration tests that test the following areas: See dependency injection of test fixtures with the xref:testing/testcontext-framework/fixture-di.adoc[TestContext framework]. - [[testing-tx]] === Transaction Management @@ -132,7 +130,6 @@ xref:testing/annotations.adoc[`@Commit`] annotation. See transaction management with the xref:testing/testcontext-framework/tx.adoc[TestContext framework]. - [[testing-support-classes]] === Support Classes for Integration Testing 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..97bb29272556 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc.adoc @@ -0,0 +1,14 @@ +[[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..932a8fd73ae1 --- /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. 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..aa371b3e7656 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/assertions.adoc @@ -0,0 +1,50 @@ +[[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..5d6f04436a85 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/integration.adoc @@ -0,0 +1,21 @@ +[[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..93dcfa5591f3 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/assertj/requests.adoc @@ -0,0 +1,84 @@ +[[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..629a051ef288 --- /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. 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 76% 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..1defcfbc8661 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,22 +20,22 @@ 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.* @Test void test() throws Exception { - MvcResult mvcResult = this.mockMvc.perform(get("/path")) - .andExpect(status().isOk()) <1> - .andExpect(request().asyncStarted()) <2> - .andExpect(request().asyncResult("body")) <3> - .andReturn(); + MvcResult mvcResult = this.mockMvc.perform(get("/path")) + .andExpect(status().isOk()) <1> + .andExpect(request().asyncStarted()) <2> + .andExpect(request().asyncResult("body")) <3> + .andReturn(); - this.mockMvc.perform(asyncDispatch(mvcResult)) <4> - .andExpect(status().isOk()) <5> - .andExpect(content().string("body")); - } + this.mockMvc.perform(asyncDispatch(mvcResult)) <4> + .andExpect(status().isOk()) <5> + .andExpect(content().string("body")); + } ---- <1> Check response status is still unchanged <2> Async processing must have started @@ -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() { @@ -70,5 +70,3 @@ Kotlin:: <4> Manually perform an ASYNC dispatch (as there is no running container) <5> Verify the final response ====== - - 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..38ff22218897 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/expectations.adoc @@ -0,0 +1,253 @@ +[[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"] +---- + standaloneSetup(SimpleController()) + .alwaysExpect(status().isOk()) + .alwaysExpect(content().contentType("application/json;charset=UTF-8")) + .build() +---- +====== + +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..ea625acabc56 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/filters.adoc @@ -0,0 +1,26 @@ +[[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"] +---- + mockMvc = standaloneSetup(PersonController()).addFilters(CharacterEncodingFilter()).build() +---- +====== + +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..e4a131e562d3 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/requests.adoc @@ -0,0 +1,180 @@ +[[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"] +---- + class MyWebTests { + + lateinit var mockMvc: MockMvc + + @BeforeEach + fun setup() { + mockMvc = standaloneSetup(AccountController()) + .defaultRequest(get("/") + .contextPath("/app").servletPath("/main") + .accept(MediaType.APPLICATION_JSON)).build() + } + } +---- +====== + +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..9d895e9de9b1 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/setup-steps.adoc @@ -0,0 +1,74 @@ +[[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"] +---- + // static import of MockMvcBuilders.standaloneSetup + + val mockMvc = standaloneSetup(MusicController()) + .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON)) + .alwaysExpect(status().isOk()) + .alwaysExpect(content().contentType("application/json;charset=UTF-8")) + .build() +---- +====== + +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"] +---- + // static import of SharedHttpSessionConfigurer.sharedHttpSession + + val mockMvc = MockMvcBuilders.standaloneSetup(TestController()) + .apply(sharedHttpSession()) + .build() + + // Use mockMvc to perform requests... +---- +====== + +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..eeaa1951bd38 --- /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..a645efe2807d --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/static-imports.adoc @@ -0,0 +1,16 @@ +[[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/spring-mvc-test-framework/vs-streaming-response.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/vs-streaming-response.adoc similarity index 77% rename from framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc rename to framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/vs-streaming-response.adoc index 6a24b6ca7c45..e44695650cb8 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/vs-streaming-response.adoc @@ -1,4 +1,4 @@ -[[spring-mvc-test-vs-streaming-response]] +[[mockmvc-vs-streaming-response]] = Streaming Responses You can use `WebTestClient` to test xref:testing/webtestclient.adoc#webtestclient-stream[streaming responses] @@ -7,10 +7,8 @@ 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}/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test with a running server]. +{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..652e29da6582 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit.adoc @@ -0,0 +1,20 @@ +[[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 77% 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..c92d0cc2dde3 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 -WebDriver. However, Geb makes things even easier by taking care of some of the -boilerplate code for us. +xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-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,15 @@ 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#mockmvc-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,10 +63,10 @@ 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 -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: +xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-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: [source,groovy] ---- 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 77% 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 6578b5d8f9cb..ba2ef39316ee 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,14 +1,13 @@ -[[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 -`org.htmlunit:htmlunit`. +First, make sure that you have included a test dependency on `org.htmlunit:htmlunit`. We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the `MockMvcWebClientBuilder`, as follows: @@ -17,7 +16,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; @@ -31,7 +30,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- lateinit var webClient: WebClient @@ -45,14 +44,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 <>. 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 @@ -63,21 +62,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 <>. 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: @@ -86,7 +85,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"); @@ -99,7 +98,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") @@ -118,7 +117,7 @@ assertions use the {assertj-docs}[AssertJ] library: ====== 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(); @@ -131,7 +130,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() @@ -144,10 +143,10 @@ 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. -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. +xref:testing/mockmvc/htmlunit/why.adoc#mockmvc-server-htmlunit-why[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. Another important factor is that https://htmlunit.sourceforge.io/javascript.html[HtmlUnit uses the Mozilla Rhino engine] to evaluate JavaScript. This means that we can also test @@ -156,7 +155,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 @@ -167,7 +166,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; @@ -181,7 +180,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- lateinit var webClient: WebClient @@ -200,7 +199,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; @@ -220,7 +219,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- lateinit var webClient: WebClient @@ -246,7 +245,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) @@ -265,9 +264,21 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed + val mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build() + + webClient = MockMvcWebClientBuilder + .mockMvcSetup(mockMvc) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only; + // the following will use MockMvc for example.com and example.org as well + .useMockMvcForHosts("example.com", "example.org") + .build() ---- ====== @@ -275,5 +286,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 80% 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 2c749b5e793f..69d76651060d 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,19 +1,19 @@ -[[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. +https://www.selenium.dev/documentation/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 Selenium WebDriver provides a very elegant API that lets us easily organize our code. To better show how it works, we explore an example in this section. -NOTE: Despite being a part of https://docs.seleniumhq.org/[Selenium], WebDriver does not -require a Selenium Server to run your tests. +NOTE: Despite being a part of https://www.selenium.dev/documentation/[Selenium], +WebDriver does not require a Selenium Server to run your tests. Suppose we need to ensure that a message is created properly. The tests involve finding the HTML form input elements, filling them out, and making various assertions. @@ -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-htmlunit3-driver`. +To use Selenium WebDriver with `MockMvc`, make sure that your project includes a test +dependency on `org.seleniumhq.selenium:htmlunit3-driver`. We can easily create a Selenium WebDriver that integrates with MockMvc by using the `MockMvcHtmlUnitDriverBuilder` as the following example shows: @@ -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 <>. 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,10 +259,11 @@ 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#mockmvc-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 -with HtmlUnit, but it is much easier with WebDriver. Consider the following +<>, we can use the Page Object Pattern with +HtmlUnit, but it is much easier with WebDriver. Consider the following `CreateMessagePage` implementation: -- @@ -270,7 +271,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> @@ -307,7 +308,7 @@ interested. These are of type `WebElement`. WebDriver's https://github.com/SeleniumHQ/selenium/wiki/PageFactory[`PageFactory`] lets us remove a lot of code from the HtmlUnit version of `CreateMessagePage` by automatically resolving each `WebElement`. The -https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`PageFactory#initElements(WebDriver,Class)`] +https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`PageFactory#initElements(WebDriver,Class)`] method automatically resolves each `WebElement` by using the field name and looking it up by the `id` or `name` of the element within the HTML page. <3> We can use the @@ -317,7 +318,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> @@ -351,7 +352,7 @@ interested. These are of type `WebElement`. WebDriver's https://github.com/SeleniumHQ/selenium/wiki/PageFactory[`PageFactory`] lets us remove a lot of code from the HtmlUnit version of `CreateMessagePage` by automatically resolving each `WebElement`. The -https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`PageFactory#initElements(WebDriver,Class)`] +https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`PageFactory#initElements(WebDriver,Class)`] method automatically resolves each `WebElement` by using the field name and looking it up by the `id` or `name` of the element within the HTML page. <3> We can use the @@ -369,7 +370,7 @@ assertions use the {assertj-docs}[AssertJ] assertion library: ====== 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 +378,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 +394,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 +408,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 +425,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 +437,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @AfterEach fun destroy() { @@ -451,7 +452,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 +463,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 +477,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 +496,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 +516,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 +542,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 +561,21 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - // Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed + val mockMvc: MockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build() + + driver = MockMvcHtmlUnitDriverBuilder + .mockMvcSetup(mockMvc) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only; + // the following will use MockMvc for example.com and example.org as well + .useMockMvcForHosts("example.com", "example.org") + .build() ---- ====== @@ -570,5 +583,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 84% 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 e3f5935f33ec..9c24ae54bfb2 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() { @@ -60,14 +60,14 @@ assume our form looks like the following snippet: ---- -How do we ensure that our form produce the correct request to create a new message? A +How do we ensure that our form produces the correct request to create a new message? A naive attempt might resemble the following: [tabs] ====== 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() } @@ -94,8 +94,8 @@ follows: ====== 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,10 +151,10 @@ 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, +To resolve the issues mentioned above, we could perform end-to-end integration testing, but this has some drawbacks. Consider testing the view that lets us page through the messages. We might need the following tests: @@ -171,7 +171,7 @@ leads to a number of additional challenges: * Testing can become slow, since each test would need to ensure that the database is in the correct state. * Since our database needs to be in a specific state, we cannot run tests in parallel. -* Performing assertions on such items as auto-generated IDs, timestamps, and others can +* Performing assertions on items such as auto-generated IDs, timestamps, and others can be difficult. These challenges do not mean that we should abandon 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..84851b2d95f6 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/overview.adoc @@ -0,0 +1,22 @@ +[[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..e0da2f74616d --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/resources.adoc @@ -0,0 +1,9 @@ +[[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/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/vs-end-to-end-integration-tests.adoc similarity index 76% rename from framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc rename to framework-docs/modules/ROOT/pages/testing/mockmvc/vs-end-to-end-integration-tests.adoc index 9b3d38ccff7d..9ca38e589d0d 100644 --- 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/mockmvc/vs-end-to-end-integration-tests.adoc @@ -1,4 +1,4 @@ -[[spring-mvc-test-vs-end-to-end-integration-tests]] +[[mockmvc-vs-end-to-end-integration-tests]] = MockMvc vs End-to-End Tests MockMvc is built on Servlet API mock implementations from the @@ -10,18 +10,18 @@ The easiest way to think about this is by starting with a blank `MockHttpServlet 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 +"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 +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}/spring-boot-features.html#boot-features-testing[Spring Boot Reference Guide]. +{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 @@ -30,17 +30,16 @@ testing, but they are a little closer to it. For example, you can isolate the we 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 +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 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 +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, +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 +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 9b6b5d61b99f..0378ad6efc50 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] -* {assertj-docs}[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. -* {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. +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 lambda expressions, 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/resttestclient.adoc b/framework-docs/modules/ROOT/pages/testing/resttestclient.adoc new file mode 100644 index 000000000000..61bc3eb6bdee --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/resttestclient.adoc @@ -0,0 +1,242 @@ +[[resttestclient]] += RestTestClient + +`RestTestClient` is an HTTP client designed for testing server applications. It wraps +Spring's xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and uses it to perform requests, +but exposes a testing facade for verifying responses. `RestTestClient` can be used to +perform end-to-end HTTP tests. It can also be used to test Spring MVC +applications without a running server via MockMvc. + + + + +[[resttestclient.setup]] +== Setup + +To set up a `RestTestClient` you need to choose a server setup to bind to. This can be one +of several MockMvc setup choices, or a connection to a live server. + + + +[[resttestclient.controller-config]] +=== Bind to Controller + +This setup allows you to test specific controller(s) via mock request and response objects, +without a running server. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + RestTestClient client = + RestTestClient.bindToController(new TestController()).build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val client = RestTestClient.bindToController(TestController()).build() +---- +====== + +[[resttestclient.context-config]] +=== Bind to `ApplicationContext` + +This setup allows you to load Spring configuration with Spring MVC +infrastructure and controller declarations and use it to handle requests via mock request +and response objects, without a running server. + +include-code::./RestClientContextTests[indent=0] + + +[[resttestclient.fn-config]] +=== Bind to Router Function + +This setup allows you to test xref:web/webmvc-functional.adoc[functional endpoints] via +mock request and response objects, without a running server. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + RouterFunction route = ... + client = RestTestClient.bindToRouterFunction(route).build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val route: RouterFunction<*> = ... + val client = RestTestClient.bindToRouterFunction(route).build() +---- +====== + +[[resttestclient.server-config]] +=== Bind to Server + +This setup connects to a running server to perform full, end-to-end HTTP tests: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build() +---- +====== + + + +[[resttestclient.client-config]] +=== Client Config + +In addition to the server setup options described earlier, you can also configure client +options, including base URL, default headers, client filters, and others. These options +are readily available following the initial `bindTo` call, as follows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + client = RestTestClient.bindToController(new TestController()) + .baseUrl("/test") + .build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + client = RestTestClient.bindToController(TestController()) + .baseUrl("/test") + .build() +---- +====== + + + + +[[resttestclient.tests]] +== Writing Tests + +xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and `RestTestClient` have +the same API up to the point of the call to `exchange()`. After that, `RestTestClient` +provides two alternative ways to verify the response: + +1. xref:resttestclient-workflow[Built-in Assertions] extend the request workflow with a chain of expectations +2. xref:resttestclient-assertj[AssertJ Integration] to verify the response via `assertThat()` statements + +TIP: See the xref:integration/rest-clients.adoc#rest-message-conversion[HTTP Message Conversion] +section for examples on how to prepare a request with any content, including form data and multipart data. + + + +[[resttestclient.workflow]] +=== Built-in Assertions + +To use the built-in assertions, remain in the workflow after the call to `exchange()`, and +use one of the expectation methods. For example: + +include-code::./RestClientWorkflowTests[tag=test,indent=0] + + +If you would like for all expectations to be asserted even if one of them fails, you can +use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is +similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in +JUnit Jupiter. + +include-code::./RestClientWorkflowTests[tag=soft-assertions,indent=0] + + +You can then choose to decode the response body through one of the following: + +* `expectBody(Class)`: Decode to single object. +* `expectBody()`: Decode to `byte[]` for xref:testing/resttestclient.adoc#resttestclient-json[JSON Content] or an empty body. + + +If the built-in assertions are insufficient, you can consume the object instead and +perform any other assertions: + +include-code::./RestClientWorkflowTests[tag=consume,indent=0] + +Or you can exit the workflow and obtain a `EntityExchangeResult`: + +include-code::./RestClientWorkflowTests[tag=result,indent=0] + + +TIP: When you need to decode to a target type with generics, look for the overloaded methods +that accept {spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`] +instead of `Class`. + + +[[resttestclient.no-content]] +==== No Content + +If the response is not expected to have content, you can assert that as follows: + +include-code::./NoContentTests[tag=emptyBody,indent=0] + +If you want to ignore the response content, the following releases the content without any assertions: + +include-code::./NoContentTests[tag=ignoreBody,indent=0] + +NOTE: Consuming the response body (for example, with `expectBody`) is required if your tests are running with +leak detection for pooled buffers. Without that, the tool will report buffers being leaked. + + +[[resttestclient.json]] +==== JSON Content + +You can use `expectBody()` without a target type to perform assertions on the raw +content rather than through higher level Object(s). + +To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]: + +include-code::./JsonTests[tag=jsonBody,indent=0] + + +To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]: + +include-code::./JsonTests[tag=jsonPath,indent=0] + + +[[resttestclient.multipart]] +==== Multipart Content + +When testing endpoints that return multipart responses, you can decode the body to a +`MultiValueMap` and assert individual parts using the `FormFieldPart` +and `FilePart` subtypes. + +include-code::./MultipartTests[tag=multipart,indent=0] + + +[[resttestclient.assertj]] +=== AssertJ Integration + +`RestTestClientResponse` is the main entry point for the AssertJ integration. +It is an `AssertProvider` that wraps the `ResponseSpec` of an exchange in order to enable +use of `assertThat()` statements. For example: + +include-code::./AssertJTests[tag=withSpec,indent=0] + + +You can also use the built-in workflow first, and then obtain an `ExchangeResult` to wrap +and continue with AssertJ. For example: + +include-code::./AssertJTests[tag=withResult,indent=0] 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 e8c56ed11fc0..81fca95595b4 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-client.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-client.adoc @@ -1,16 +1,32 @@ [[spring-mvc-test-client]] = Testing Client Applications -You can use client-side tests to test code that internally uses the `RestTemplate`. The -idea is to declare expected requests and to provide "`stub`" responses so that you can -focus on testing the code in isolation (that is, without running a server). The following -example shows how to do so: +To test code that uses the `RestClient` or `RestTemplate`, you can use a mock web server, such as +https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or +https://wiremock.org/[WireMock]. Mock web servers accept requests over HTTP like a regular +server, and that means you can test with the same HTTP client that is also configured in +the same way as in production, which is important because there are often subtle +differences in the way different clients handle network I/O. Another advantage of mock +web servers is the ability to simulate specific network issues and conditions at the +transport level, in combination with the client used in production. + +In addition to dedicated mock web servers, historically the Spring Framework has provided +a built-in option to test `RestClient` or `RestTemplate` through `MockRestServiceServer`. +This relies on configuring the client under test with a custom `ClientHttpRequestFactory` +backed by the mock server that is in turn set up to expect requests and send "`stub`" +responses so that you can focus on testing the code in isolation, without running a server. + +TIP: `MockRestServiceServer` predates the existence of mock web servers. At present, we +recommend using mock web servers for more complete testing of the transport layer and +network conditions. + +The following example shows an example of using `MockRestServiceServer`: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- RestTemplate restTemplate = new RestTemplate(); @@ -24,7 +40,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val restTemplate = RestTemplate() @@ -55,14 +71,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 +93,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 +108,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val restTemplate = RestTemplate() @@ -122,7 +138,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 +148,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 +165,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 +183,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val restTemplate = RestTemplate() @@ -193,9 +209,10 @@ 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]] == Static Imports 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 27dbf6ed50f5..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 {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",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 -{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",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 b292ec168a3e..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 {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/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 0d6bcab7ca33..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 {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/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 cb9c8e97e8d6..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-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/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 2abf6919d5a5..000000000000 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-options.adoc +++ /dev/null @@ -1,181 +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 var 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 e179a8364a27..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 {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",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 {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/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/support-jdbc.adoc b/framework-docs/modules/ROOT/pages/testing/support-jdbc.adoc index cc09b76658d9..6d76ab2e18a2 100644 --- a/framework-docs/modules/ROOT/pages/testing/support-jdbc.adoc +++ b/framework-docs/modules/ROOT/pages/testing/support-jdbc.adoc @@ -1,6 +1,7 @@ [[integration-testing-support-jdbc]] = JDBC Testing Support + [[integration-testing-support-jdbc-test-utils]] == JdbcTestUtils @@ -31,5 +32,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.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework.adoc index 0cf24faa9aa1..b7eae8ed9221 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework.adoc @@ -9,18 +9,17 @@ deal of importance on convention over configuration, with reasonable defaults th can override through annotation-based configuration. In addition to generic testing infrastructure, the TestContext framework provides -explicit support for JUnit 4, JUnit Jupiter (AKA JUnit 5), and TestNG. For JUnit 4 and -TestNG, Spring provides `abstract` support classes. Furthermore, Spring provides a custom -JUnit `Runner` and custom JUnit `Rules` for JUnit 4 and a custom `Extension` for JUnit -Jupiter that let you write so-called POJO test classes. POJO test classes are not -required to extend a particular class hierarchy, such as the `abstract` support classes. +explicit support for JUnit Jupiter, JUnit 4, and TestNG. For JUnit 4 and TestNG, Spring +provides `abstract` support classes. Furthermore, Spring provides a custom JUnit `Runner` +and custom JUnit `Rules` for JUnit 4 and a custom `Extension` for JUnit Jupiter that let +you write so-called POJO test classes. POJO test classes are not required to extend a +particular class hierarchy, such as the `abstract` support classes. The following section provides an overview of the internals of the TestContext framework. If you are interested only in using the framework and are not interested in extending it with your own custom listeners or custom loaders, feel free to go directly to the configuration (xref:testing/testcontext-framework/ctx-management.adoc[context management], -xref:testing/testcontext-framework/fixture-di.adoc[dependency injection], xref:testing/testcontext-framework/tx.adoc[transaction management] -), xref:testing/testcontext-framework/support-classes.adoc[support classes], and +xref:testing/testcontext-framework/fixture-di.adoc[dependency injection], +xref:testing/testcontext-framework/tx.adoc[transaction management]), +xref:testing/testcontext-framework/support-classes.adoc[support classes], and xref:testing/annotations.adoc[annotation support] sections. - - 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 5348b383c4cf..df049c3eb6e2 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc @@ -43,6 +43,16 @@ alternative, you can set the same property via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. ==== +[TIP] +==== +JPA's `@PersistenceContext` and `@PersistenceUnit` annotations cannot be used to perform +dependency injection within test classes in AOT mode. + +However, as of Spring Framework 7.0, you can inject an `EntityManager` or +`EntityManagerFactory` into tests using `@Autowired` instead of `@PersistenceContext` and +`@PersistenceUnit`, respectively. +==== + [NOTE] ==== The `@ContextHierarchy` annotation is not supported in AOT mode. 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 b27d2d98c5b5..54dc7c1262df 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,11 +1,11 @@ [[testcontext-application-events]] = Application Events -Since Spring Framework 5.3.3, 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 -the `ApplicationEvents` API which allows you to process the events as a +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 the `ApplicationEvents` API which allows you to process the events as a `java.util.Stream`. To use `ApplicationEvents` in your tests, do the following. @@ -16,38 +16,39 @@ To use `ApplicationEvents` in your tests, do the following. that `ApplicationEventsTestExecutionListener` is registered by default and only needs to be manually registered if you have custom configuration via `@TestExecutionListeners` that does not include the default listeners. -* Annotate a field of type `ApplicationEvents` with `@Autowired` and use that instance of - `ApplicationEvents` in your test and lifecycle methods (such as `@BeforeEach` and - `@AfterEach` methods in JUnit Jupiter). -** When using the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter], you may declare a method - parameter of type `ApplicationEvents` in a test or lifecycle method as an alternative - to an `@Autowired` field in the test class. +* When using the + xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter], + declare a method parameter of type `ApplicationEvents` in a `@Test`, `@BeforeEach`, or + `@AfterEach` method. +** Since `ApplicationEvents` is scoped to the lifecycle of the current test method, this + is the recommended approach. +* Alternatively, you can annotate a field of type `ApplicationEvents` with `@Autowired` + and use that instance of `ApplicationEvents` in your test and lifecycle methods. + +NOTE: `ApplicationEvents` is registered with the `ApplicationContext` as a _resolvable +dependency_ which is scoped to the lifecycle of the current test method. Consequently, +`ApplicationEvents` cannot be accessed outside the lifecycle of a test method and cannot be +`@Autowired` into the constructor of a test class. The following test class uses the `SpringExtension` for JUnit Jupiter and -{assertj-docs}[AssertJ] to assert the types of application events -published while invoking a method in a Spring-managed component: +{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 /* ... */ [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @SpringJUnitConfig(/* ... */) @RecordApplicationEvents // <1> class OrderServiceTests { - @Autowired - OrderService orderService; - - @Autowired - ApplicationEvents events; // <2> - @Test - void submitOrder() { + void submitOrder(@Autowired OrderService service, ApplicationEvents events) { // <2> // Invoke method in OrderService that publishes an event - orderService.submitOrder(new Order(/* ... */)); + service.submitOrder(new Order(/* ... */)); // Verify that an OrderSubmitted event was published long numEvents = events.stream(OrderSubmitted.class).count(); // <3> assertThat(numEvents).isEqualTo(1); @@ -60,22 +61,16 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @SpringJUnitConfig(/* ... */) @RecordApplicationEvents // <1> class OrderServiceTests { - @Autowired - lateinit var orderService: OrderService - - @Autowired - lateinit var events: ApplicationEvents // <2> - @Test - fun submitOrder() { + fun submitOrder(@Autowired service: OrderService, events: ApplicationEvents) { // <2> // Invoke method in OrderService that publishes an event - orderService.submitOrder(Order(/* ... */)) + service.submitOrder(Order(/* ... */)) // Verify that an OrderSubmitted event was published val numEvents = events.stream(OrderSubmitted::class).count() // <3> assertThat(numEvents).isEqualTo(1) @@ -90,4 +85,3 @@ Kotlin:: See the {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 index 243de7e3bd4f..0c108878565c 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc @@ -2,7 +2,9 @@ = 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 one or more fields in the test class. +`ApplicationContext` for a test class, by annotating the test class, one or more +non-static fields in the test class, or one or more parameters in the constructor for 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` @@ -22,12 +24,12 @@ https://site.mockito.org/[Mockito] third-party library. The three annotations mentioned above build upon the `@BeanOverride` meta-annotation and associated infrastructure, which allows one to define custom bean overriding variants. -To create custom bean override support, the following is needed: +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 `OverrideMetadata` implementations provided by the processor +* 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. @@ -41,19 +43,39 @@ The `spring-test` module registers implementations of the latter two {spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories` properties file]. -The bean overriding infrastructure searches in test classes for any field meta-annotated -with `@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is -responsible for registering appropriate `OverrideMetadata`. - -The internal `BeanOverrideBeanFactoryPostProcessor` then uses that information to alter -the test's `ApplicationContext` by registering and replacing bean definitions as defined -by the corresponding `BeanOverrideStrategy`: - -* `REPLACE_DEFINITION`: Replaces the bean definition. Throws an exception if a - corresponding bean definition does not exist. -* `REPLACE_OR_CREATE_DEFINITION`: Replaces the bean definition if it exists. Creates a - new bean definition if a corresponding bean definition does not exist. -* `WRAP_BEAN`: Retrieves the original bean instance and wraps it. +The bean overriding infrastructure searches for annotations on test classes, non-static +fields in test classes, and parameters in test class constructors 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] +==== +When replacing a non-singleton bean, the non-singleton bean will be replaced with a +singleton bean corresponding to bean override instance created by the applicable +`BeanOverrideHandler`, and the corresponding bean definition will be converted to a +`singleton`. Consequently, if a handler overrides a `prototype` or scoped bean, the +overridden bean will be treated as a `singleton`. + +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] ==== @@ -63,9 +85,73 @@ heuristics it can perform to locate a bean. Either the `BeanOverrideProcessor` c 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`. +Typically, the bean is selected "by type" by the `BeanOverrideFactoryPostProcessor`. Alternatively, the user can directly provide the bean name in the custom annotation. -Some `BeanOverrideProcessor` implementations could also internally compute a bean name -based on a convention or another advanced method. +`BeanOverrideProcessor` implementations may also internally compute a bean name based on +a convention or some other method. ==== + +[[testcontext-bean-overriding-aop-proxies]] +== Bean Overrides and Spring AOP Proxies + +Beans in a Spring `ApplicationContext` are frequently wrapped in an AOP proxy — for +example, to support `@Transactional`, `@Cacheable`, or `@Retryable` semantics. Whether an +overridden bean retains such a proxy depends on the `BeanOverrideStrategy` used to create +the override. + +* Overrides that use the `REPLACE` or `REPLACE_OR_CREATE` strategy (such as `@TestBean` + and `@MockitoBean`) register their override instance directly as a manual singleton, + which bypasses the container's normal bean post-processing. Consequently, the override + instance is a bare object: none of the AOP advice that would otherwise apply to the + original bean (`@Transactional`, `@Cacheable`, `@Retryable`, method security, and so + on) is present. +* Overrides that use the `WRAP` strategy (such as `@MockitoSpyBean`) capture an early + reference to the original bean and use it to create the override instance, before the + rest of the container's post-processors — including the one responsible for creating + AOP proxies — have run. Consequently, if the original bean would have been proxied, + that proxy is still created, but it now wraps the override instance instead of the + original bean. The bean that ends up in the `ApplicationContext`, and that is injected + into collaborating beans and test classes, is therefore the AOP proxy, with the + override instance as its target — not the bare override instance itself. + +The following diagrams illustrate the resulting shape of the bean for each strategy, from +the perspective of a caller invoking a method on the injected bean. + +With the `REPLACE` or `REPLACE_OR_CREATE` strategy, there is no AOP proxy at all: the +caller invokes the override instance directly. + +[source] +---- +caller + │ + ▼ +[ override instance ] +---- + +With the `WRAP` strategy, any AOP proxy that would normally have wrapped the original +bean is still created, but now wraps the override instance instead: + +[source] +---- +caller + │ + ▼ +[ AOP proxy ] (for example, retry, caching, or transaction advice) + │ + │ delegates to its target + ▼ +[ override instance ] (for example, a Mockito spy created by @MockitoSpyBean) +---- + +For a `WRAP`-based override such as `@MockitoSpyBean`, the "wrapping" performed by the +AOP proxy is unrelated to the manner in which the resulting Mockito spy itself "wraps" +the original bean instance it was created from. The proxy shown above determines which +object a caller actually invokes, whereas the spy's relationship to the original +instance only determines what happens when an unstubbed method is invoked on the spy: it +falls through to that instance's real behavior. + +This distinction has practical consequences when combining bean overrides with Mockito's +stubbing and verification APIs. See +xref:testing/annotations/integration-spring/annotation-mockitobean.adoc#spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies[`@MockitoSpyBean` +and Spring AOP Proxies] for details. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bootstrapping.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bootstrapping.adoc index bf3be0e663a6..51f0861f0551 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bootstrapping.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bootstrapping.adoc @@ -19,8 +19,6 @@ meta-annotation. If a bootstrapper is not explicitly configured by using `WebTestContextBootstrapper` is used, depending on the presence of `@WebAppConfiguration`. Since the `TestContextBootstrapper` SPI is likely to change in the future (to accommodate -new requirements), we strongly encourage implementers not to implement this interface +new requirements), we strongly encourage implementors not to implement this interface directly but rather to extend `AbstractTestContextBootstrapper` or one of its concrete subclasses instead. - - 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..c56bc8d25232 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 { @@ -49,7 +49,6 @@ Kotlin:: <1> Injecting the `ApplicationContext`. ====== - Similarly, if your test is configured to load a `WebApplicationContext`, you can inject the web application context into your test, as follows: @@ -57,7 +56,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 +72,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitWebConfig // <1> class MyWebAppTest { @@ -87,7 +86,6 @@ Kotlin:: <2> Injecting the `WebApplicationContext`. ====== - Dependency injection by using `@Autowired` is provided by the `DependencyInjectionTestExecutionListener`, which is configured by default (see xref:testing/testcontext-framework/fixture-di.adoc[Dependency Injection of Test Fixtures]). @@ -96,22 +94,24 @@ Dependency injection by using `@Autowired` is provided by the Test classes that use the TestContext framework do not need to extend any particular class or implement a specific interface to configure their application context. Instead, configuration is achieved by declaring the `@ContextConfiguration` annotation at the -class level. If your test class does not explicitly declare application context resource -locations or component classes, the configured `ContextLoader` determines how to load a -context from a default location or default configuration classes. In addition to context -resource locations and component classes, an application context can also be configured -through application context initializers. - -The following sections explain how to use Spring's `@ContextConfiguration` annotation to -configure a test `ApplicationContext` by using XML configuration files, Groovy scripts, -component classes (typically `@Configuration` classes), or context initializers. -Alternatively, you can implement and configure your own custom `SmartContextLoader` for -advanced use cases. - -* xref:testing/testcontext-framework/ctx-management/xml.adoc[Context Configuration with XML resources] -* xref:testing/testcontext-framework/ctx-management/groovy.adoc[Context Configuration with Groovy Scripts] +class level. If your test class does not explicitly declare component classes or resource +locations, the configured `ContextLoader` determines how to load a context from _default_ +configuration classes or a _default_ location. In addition to component classes and +context resource locations, an application context can also be configured through +xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[context customizers] +or xref:testing/testcontext-framework/ctx-management/initializers.adoc[context initializers]. + +The following sections explain how to use `@ContextConfiguration` and related annotations +to configure a test `ApplicationContext` by using component classes (typically +`@Configuration` classes), XML configuration files, Groovy scripts, context customizers, +or context initializers. Alternatively, you can implement and configure your own custom +`SmartContextLoader` for advanced use cases. + * xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[Context Configuration with Component Classes] -* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing XML, Groovy Scripts, and Component Classes] +* xref:testing/testcontext-framework/ctx-management/xml.adoc[Context Configuration with XML Resources] +* xref:testing/testcontext-framework/ctx-management/groovy.adoc[Context Configuration with Groovy Scripts] +* xref:testing/testcontext-framework/ctx-management/default-config.adoc[Default Context Configuration] +* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing Component Classes, XML, and Groovy Scripts] * xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[Context Configuration with Context Customizers] * xref:testing/testcontext-framework/ctx-management/initializers.adoc[Context Configuration with Context Initializers] * xref:testing/testcontext-framework/ctx-management/inheritance.adoc[Context Configuration Inheritance] @@ -122,4 +122,3 @@ advanced use cases. * xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] * xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[Context Failure Threshold] * xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[Context Hierarchies] - 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..aa749f6b78df 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`) @@ -31,10 +31,10 @@ under a key that is based solely on those locations. So, if `TestClassB` also de `{"app-config.xml", "test-config.xml"}` for its locations (either explicitly or implicitly through inheritance) but does not define `@WebAppConfiguration`, a different `ContextLoader`, different active profiles, different context initializers, different -test property sources, or a different parent context, then the same `ApplicationContext` -is shared by both test classes. This means that the setup cost for loading an application -context is incurred only once (per test suite), and subsequent test execution is much -faster. +context customizers, different test or dynamic property sources, or a different parent +context, then the same `ApplicationContext` is shared by both test classes. This means +that the setup cost for loading an application context is incurred only once (per test +suite), and subsequent test execution is much faster. .Test suites and forked processes [NOTE] @@ -71,10 +71,11 @@ the underlying context cache, you can set the log level for the In the unlikely case that a test corrupts the application context and requires reloading (for example, by modifying a bean definition or the state of an application object), you can annotate your test class or test method with `@DirtiesContext` (see the discussion of -`@DirtiesContext` in xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] -). This instructs Spring to remove the context from the cache and rebuild -the application context before running the next test that requires the same application -context. Note that support for the `@DirtiesContext` annotation is provided by the +`@DirtiesContext` in +xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations]). +This instructs Spring to remove the context from the cache and rebuild the application +context before running the next test that requires the same application context. Note +that support for the `@DirtiesContext` annotation is provided by the `DirtiesContextBeforeModesTestExecutionListener` and the `DirtiesContextTestExecutionListener`, which are enabled by default. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc index 1698c6169291..f1af4efc3c9f 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc @@ -1,5 +1,5 @@ [[testcontext-context-customizers]] -= Configuration Configuration with Context Customizers += Context Configuration with Context Customizers A `ContextCustomizer` is responsible for customizing the supplied `ConfigurableApplicationContext` after bean definitions have been loaded into the context diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-pausing.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-pausing.adoc new file mode 100644 index 000000000000..7b0c30b55741 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-pausing.adoc @@ -0,0 +1,45 @@ +[[testcontext-ctx-management-pausing]] += Context Pausing + +As of Spring Framework 7.0, an `ApplicationContext` stored in the context cache (see +xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]) may be +_paused_ when it is no longer actively in use and automatically _restarted_ the next time +the context is retrieved from the cache. Specifically, the latter will restart all +auto-startup beans in the application context, effectively restoring the lifecycle state. +This ensures that background processes within the context are not actively running while +the context is not used by tests. For example, JMS listener containers, scheduled tasks, +and any other components in the context that implement `Lifecycle` or `SmartLifecycle` +will be in a "stopped" state until the context is used again by a test. Note, however, +that `SmartLifecycle` components can opt out of pausing by returning `false` from +`SmartLifecycle#isPauseable()`. + +You can control whether inactive application contexts should be paused by setting the +`PauseMode` to one of the following supported values. + +`ALWAYS` :: Always pause inactive application contexts. +`ON_CONTEXT_SWITCH` :: Only pause inactive application contexts if the next context + retrieved from the context cache is a different context. +`NEVER` :: Never pause inactive application contexts, effectively disabling the pausing + feature of the context cache. + +The `PauseMode` defaults to `ON_CONTEXT_SWITCH`, but it can be changed from the command +line or a build script by setting a JVM system property named +`spring.test.context.cache.pause` to one of the supported values (case insensitive). As +an alternative, you can set the property via the +xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. + +For example, if you want inactive application contexts to always be paused, you can +switch from the default `ON_CONTEXT_SWITCH` mode to `ALWAYS` by setting the +`spring.test.context.cache.pause` system property to `always`. + +```shell +-Dspring.test.context.cache.pause=always +``` +Similarly, if you encounter issues with `Lifecycle` components that cannot or should not +opt out of pausing, or if you discover that your test suite runs more slowly due to the +pausing and restarting of application contexts, you can disable the pausing feature by +setting the `spring.test.context.cache.pause` system property to `never`. + +```shell +-Dspring.test.context.cache.pause=never +``` diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/default-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/default-config.adoc new file mode 100644 index 000000000000..e8343d10e320 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/default-config.adoc @@ -0,0 +1,53 @@ +[[testcontext-ctx-management-default-config]] += Default Context Configuration + +As explained in the sections on +xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[component classes], +xref:testing/testcontext-framework/ctx-management/xml.adoc[XML resources], and +xref:testing/testcontext-framework/ctx-management/groovy.adoc[Groovy scripts], the +TestContext framework will attempt to locate _default_ context configuration if you do +not explicitly specify `@Configuration` classes, XML configuration files, or Groovy +scripts from which the test's `ApplicationContext` should be loaded. + +However, due to a bug in the detection algorithm, default context configuration for a +superclass or enclosing class is currently ignored if the type hierarchy or enclosing +class hierarchy (for `@Nested` test classes) does not declare `@ContextConfiguration`. + +Beginning with Spring Framework 7.1, the TestContext framework will reliably detect +**all** _default_ context configuration within a type hierarchy or enclosing class +hierarchy above a given test class in such scenarios. Consequently, test suites may +encounter issues after the upgrade to 7.1. For example, if a static nested +`@Configuration` class in a superclass or enclosing class is ignored due to the +aforementioned bug, after the bug has been fixed in 7.1 that `@Configuration` class will +no longer be ignored, which may lead to unexpected beans in the resulting +`ApplicationContext` our outright failures in tests. + +In the interim, the TestContext framework logs a warning whenever it encounters _default_ +context configuration that is currently ignored — for example, a `@Configuration` class +or XML configuration file. The remainder of this section provides guidance on how to +address such issues if you encounter warnings in your test suite. + +[TIP] +==== +Annotating the affected subclass or `@Nested` class with `@ContextConfiguration` allows +you to take matters into your own hands and specify which classes in the hierarchy are +actually intended to contribute context configuration. +==== + +If you do not want static nested `@Configuration` classes to be processed, you can: + +- Remove the `@Configuration` declaration. +- Apply `@ContextConfiguration` only where you actually want such classes to be processed. +- Move the static nested `@Configuration` classes to standalone top-level classes so that + they cannot be accidentally interpreted as _default_ configuration classes. + +Similarly, if you encounter issues with _default_ XML configuration files or Groovy +scripts being detected and you do not want them to be processed, you can: + +- Apply `@ContextConfiguration` only where you actually want such resources to be + processed. +- Rename the resource files to something that does not match the default naming + convention (such as `*-context.xml` for XML configuration) so that they cannot be + accidentally interpreted as _default_ configuration files. +- Move the affected resource files to a different package or filesystem location within + your project. 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 a3936d68824c..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 @@ -2,39 +2,51 @@ = Context Configuration with Dynamic Property Sources The Spring TestContext Framework provides support for _dynamic_ properties via the -`@DynamicPropertySource` annotation and the `DynamicPropertyRegistry`. +`DynamicPropertyRegistry`, the `@DynamicPropertySource` annotation, and the +`DynamicPropertyRegistrar` API. [NOTE] ==== -The `@DynamicPropertySource` annotation and its supporting infrastructure were originally -designed to allow properties from {testcontainers-site}[Testcontainers] based tests to be -exposed easily to Spring integration tests. However, this feature 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`. +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` can be applied to -`static` methods in integration test classes or to `@Bean` methods in test -`@Configuration` 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. + +[[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. +is resolved. Typically, method references are used to supply values. The following +sections provide examples of how to use the `DynamicPropertyRegistry`. -Methods in integration test classes that are annotated with `@DynamicPropertySource` must -be `static` and must accept a single `DynamicPropertyRegistry` argument. -`@Bean` methods annotated with `@DynamicPropertySource` may either accept an argument of -type `DynamicPropertyRegistry` or access a `DynamicPropertyRegistry` instance autowired -into their enclosing `@Configuration` class. Note, however, that `@Bean` methods which -interact with a `DynamicPropertyRegistry` are not required to be annotated with -`@DynamicPropertySource` unless they need to enforce eager initialization of the bean -within the context. See the class-level javadoc for `DynamicPropertyRegistry` for details. +[[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] ==== @@ -57,7 +69,7 @@ example, via `@Value("${redis.host}")` and `@Value("${redis.port}")`, respective ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(/* ... */) @Testcontainers @@ -80,7 +92,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(/* ... */) @Testcontainers @@ -107,60 +119,71 @@ Kotlin:: ---- ====== -The following example demonstrates how to use `DynamicPropertyRegistry` and -`@DynamicPropertySource` with a `@Bean` method. 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}")`. The value of the `api.url` property -will be dynamically retrieved from the `ApiServer` bean. + +[[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",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration class TestConfig { @Bean - @DynamicPropertySource - ApiServer apiServer(DynamicPropertyRegistry registry) { - ApiServer apiServer = new ApiServer(); - registry.add("api.url", apiServer::getUrl); - return apiServer; + 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",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class TestConfig { @Bean - @DynamicPropertySource - fun apiServer(registry: DynamicPropertyRegistry): ApiServer { - val apiServer = ApiServer() - registry.add("api.url", apiServer::getUrl) - return apiServer + fun apiServer(): ApiServer { + return ApiServer() + } + + @Bean + fun apiPropertiesRegistrar(apiServer: ApiServer): DynamicPropertyRegistrar { + return registry -> registry.add("api.url", apiServer::getUrl) } } ---- ====== - -NOTE: The use of `@DynamicPropertySource` on the `@Bean` method is optional and results -in the `ApiServer` bean being eagerly initialized so that other beans in the context can -be given access to the dynamic properties sourced from the `ApiServer` bean when those -other beans are initialized. - -[[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. - 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 f1d52d53c03a..2fcd2bb94883 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) @@ -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 { @@ -543,3 +543,85 @@ Kotlin:: ---- ====== +The following example demonstrates how to implement and register a custom +`SystemPropertyOverrideActiveProfilesResolver` that allows the `spring.profiles.active` +property (when configured as a JVM system property) to override profiles configured via +`@ActiveProfiles`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + // profiles resolved programmatically via a custom resolver that + // allows "spring.profiles.active" to override @ActiveProfiles + @ActiveProfiles( + resolver = SystemPropertyOverrideActiveProfilesResolver.class, + inheritProfiles = false) + class TransferServiceTest extends AbstractIntegrationTest { + // test body + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + // profiles resolved programmatically via a custom resolver that + // allows "spring.profiles.active" to override @ActiveProfiles + @ActiveProfiles( + resolver = SystemPropertyOverrideActiveProfilesResolver::class, + inheritProfiles = false) + class TransferServiceTest : AbstractIntegrationTest() { + // test body + } +---- +====== + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary",fold="-imports"] +---- + import org.springframework.core.env.AbstractEnvironment; + import org.springframework.test.context.support.DefaultActiveProfilesResolver; + import org.springframework.util.StringUtils; + + public class SystemPropertyOverrideActiveProfilesResolver extends DefaultActiveProfilesResolver { + + @Override + public String[] resolve(Class testClass) { + String profiles = System.getProperty(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME); + if (StringUtils.hasText(profiles)) { + return StringUtils.commaDelimitedListToStringArray( + StringUtils.trimAllWhitespace(profiles)); + } + return super.resolve(testClass); + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary",fold="-imports"] +---- + import org.springframework.core.env.AbstractEnvironment + import org.springframework.test.context.support.DefaultActiveProfilesResolver + import org.springframework.util.StringUtils + + class SystemPropertyOverrideActiveProfilesResolver : DefaultActiveProfilesResolver() { + + override fun resolve(testClass: Class<*>): Array { + val profiles = System.getProperty(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME) + if (StringUtils.hasText(profiles)) { + return StringUtils.commaDelimitedListToStringArray( + StringUtils.trimAllWhitespace(profiles) + ) + } + return super.resolve(testClass) + } + } +---- +====== 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 443a89fb38db..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 @@ -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 22953ed289cb..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 -{spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc. +[[testcontext-ctx-management-ctx-hierarchies-with-bean-overrides]] +**Context hierarchies with bean overrides** -- +When `@ContextHierarchy` is used in conjunction with +xref:testing/testcontext-framework/bean-overriding.adoc[bean overrides] such as +`@TestBean`, `@MockitoBean`, or `@MockitoSpyBean`, it may be desirable or necessary to +have the override applied to a single level in the context hierarchy. To achieve that, +the bean override must specify a context name that matches a name configured via the +`name` attribute in `@ContextConfiguration`. + +The following test class configures the name of the second hierarchy level to be +`"user-config"` and simultaneously specifies that the `UserService` should be wrapped in +a Mockito spy in the context named `"user-config"`. Consequently, Spring will only +attempt to create the spy in the `"user-config"` context and will not attempt to create +the spy in the parent context. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = AppConfig.class), + @ContextConfiguration(classes = UserConfig.class, name = "user-config") + }) + class IntegrationTests { + + @MockitoSpyBean(contextName = "user-config") + UserService userService; + + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension::class) + @ContextHierarchy( + ContextConfiguration(classes = [AppConfig::class]), + ContextConfiguration(classes = [UserConfig::class], name = "user-config")) + class IntegrationTests { + + @MockitoSpyBean(contextName = "user-config") + lateinit var userService: UserService + + // ... + } +---- +====== +When applying bean overrides in different levels of the context hierarchy, you may need +to have all of the bean override instances injected into the test class in order to +interact with them — for example, to configure stubbing for mocks. However, `@Autowired` +will always inject a matching bean found in the lowest level of the context hierarchy. +Thus, to inject bean override instances from specific levels in the context hierarchy, +you need to annotate fields with appropriate bean override annotations and configure the +name of the context level. + +The following test class configures the names of the hierarchy levels to be `"parent"` +and `"child"`. It also declares two `PropertyService` fields that are configured to +create or replace `PropertyService` beans with Mockito mocks in the respective contexts, +named `"parent"` and `"child"`. Consequently, the mock from the `"parent"` context will +be injected into the `propertyServiceInParent` field, and the mock from the `"child"` +context will be injected into the `propertyServiceInChild` field. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = ParentConfig.class, name = "parent"), + @ContextConfiguration(classes = ChildConfig.class, name = "child") + }) + class IntegrationTests { + + @MockitoBean(contextName = "parent") + PropertyService propertyServiceInParent; + + @MockitoBean(contextName = "child") + PropertyService propertyServiceInChild; + + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension::class) + @ContextHierarchy( + ContextConfiguration(classes = [ParentConfig::class], name = "parent"), + ContextConfiguration(classes = [ChildConfig::class], name = "child")) + class IntegrationTests { + + @MockitoBean(contextName = "parent") + lateinit var propertyServiceInParent: PropertyService + + @MockitoBean(contextName = "child") + lateinit var propertyServiceInChild: PropertyService + + // ... + } +---- +====== +-- diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/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 af460ea84f06..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 @@ -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/mixed-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/mixed-config.adoc index c1b97b4a7a2e..2608be393c42 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/mixed-config.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/mixed-config.adoc @@ -1,33 +1,32 @@ [[testcontext-ctx-management-mixed-config]] -= Mixing XML, Groovy Scripts, and Component Classes += Mixing Component Classes, XML, and Groovy Scripts -It may sometimes be desirable to mix XML configuration files, Groovy scripts, and -component classes (typically `@Configuration` classes) to configure an -`ApplicationContext` for your tests. For example, if you use XML configuration in -production, you may decide that you want to use `@Configuration` classes to configure +It may sometimes be desirable to mix component classes (typically `@Configuration` +classes), XML configuration files, or Groovy scripts to configure an `ApplicationContext` +for your tests. For example, if you use XML configuration in production for legacy +reasons, you may decide that you want to use `@Configuration` classes to configure specific Spring-managed components for your tests, or vice versa. Furthermore, some third-party frameworks (such as Spring Boot) provide first-class support for loading an `ApplicationContext` from different types of resources -simultaneously (for example, XML configuration files, Groovy scripts, and -`@Configuration` classes). The Spring Framework, historically, has not supported this for -standard deployments. Consequently, most of the `SmartContextLoader` implementations that -the Spring Framework delivers in the `spring-test` module support only one resource type -for each test context. However, this does not mean that you cannot use both. One -exception to the general rule is that the `GenericGroovyXmlContextLoader` and +simultaneously (for example, `@Configuration` classes, XML configuration files, and +Groovy scripts). The Spring Framework, historically, has not supported this for standard +deployments. Consequently, most of the `SmartContextLoader` implementations that the +Spring Framework delivers in the `spring-test` module support only one resource type for +each test context. However, this does not mean that you cannot use a mixture of resource +types. One exception to the general rule is that the `GenericGroovyXmlContextLoader` and `GenericGroovyXmlWebContextLoader` support both XML configuration files and Groovy scripts simultaneously. Furthermore, third-party frameworks may choose to support the -declaration of both `locations` and `classes` through `@ContextConfiguration`, and, with +declaration of both `classes` and `locations` through `@ContextConfiguration`, and, with the standard testing support in the TestContext framework, you have the following options. -If you want to use resource locations (for example, XML or Groovy) and `@Configuration` -classes to configure your tests, you must pick one as the entry point, and that one must -include or import the other. For example, in XML or Groovy scripts, you can include -`@Configuration` classes by using component scanning or defining them as normal Spring -beans, whereas, in a `@Configuration` class, you can use `@ImportResource` to import XML -configuration files or Groovy scripts. Note that this behavior is semantically equivalent +If you want to use `@Configuration` classes and resource locations (for example, XML or +Groovy) to configure your tests, you must pick one as the entry point, and that one must +import or include the other. For example, in a `@Configuration` class, you can use +`@ImportResource` to import XML configuration files or Groovy scripts; whereas, in XML or +Groovy scripts, you can include `@Configuration` classes by using component scanning or +defining them as normal Spring beans. Note that this behavior is semantically equivalent to how you configure your application in production: In production configuration, you -define either a set of XML or Groovy resource locations or a set of `@Configuration` -classes from which your production `ApplicationContext` is loaded, but you still have the -freedom to include or import the other type of configuration. - +define either a set of `@Configuration` classes or a set of XML or Groovy resource +locations from which your production `ApplicationContext` is loaded, but you still have +the freedom to import or include the other type of configuration. 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 bb83c4d4da65..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. @@ -217,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( @@ -231,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", @@ -261,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` @@ -274,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 @@ -291,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 @@ -316,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 @@ -333,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..46742aa8e2c3 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 @@ -1,5 +1,5 @@ [[testcontext-ctx-management-xml]] -= Context Configuration with XML resources += Context Configuration with XML Resources To load an `ApplicationContext` for your tests by using XML configuration files, annotate your test class with `@ContextConfiguration` and configure the `locations` attribute with @@ -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 b3bd66ff7c7f..0ef8443eaea3 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 @@ -14,6 +14,7 @@ Although it is very useful to initialize a database for testing _once_ when the database _during_ integration tests. The following sections explain how to run SQL scripts programmatically and declaratively during integration tests. + [[testcontext-executing-sql-programmatically]] == Executing SQL scripts programmatically @@ -50,7 +51,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 +67,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test fun databaseTest() { @@ -88,6 +89,7 @@ and xref:testing/testcontext-framework/support-classes.adoc#testcontext-support- internally use a `ResourceDatabasePopulator` to run SQL scripts. See the Javadoc for the various `executeSqlScript(..)` methods for further details. + [[testcontext-executing-sql-declaratively]] == Executing SQL scripts declaratively with @Sql @@ -122,6 +124,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: @@ -129,7 +134,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") @@ -150,7 +155,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig @Sql("/test-schema.sql") @@ -207,7 +212,7 @@ The following example shows how to use `@Sql` as a repeatable annotation: ====== 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 = "`")) @@ -219,7 +224,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test @Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")) @@ -241,13 +246,13 @@ but you may need to use `@SqlGroup` for compatibility with other JVM languages. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Test @SqlGroup({ @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")), @Sql("/test-user-data.sql") - )} + }) void userTest() { // run code that uses the test schema and test data } @@ -255,7 +260,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test @SqlGroup( @@ -280,7 +285,7 @@ database state), you can set the `executionPhase` attribute in `@Sql` to ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Test @Sql( @@ -300,7 +305,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Test @Sql("create-test-data.sql", @@ -326,7 +331,7 @@ declaration to `BEFORE_TEST_CLASS` or `AFTER_TEST_CLASS`, as the following examp ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig @Sql(scripts = "/test-schema.sql", executionPhase = BEFORE_TEST_CLASS) @@ -347,7 +352,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig @Sql("/test-schema.sql", executionPhase = BEFORE_TEST_CLASS) @@ -424,7 +429,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 @@ -458,7 +463,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestDatabaseConfig::class) @Transactional @@ -495,13 +500,12 @@ 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 your test class or test method with `@SqlMergeMode(MERGE)`. To disable merging for a specific test method (or specific test subclass), you can switch back to the default mode -via `@SqlMergeMode(OVERRIDE)`. Consult the xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[`@SqlMergeMode` annotation documentation section] - for examples and further details. - - +via `@SqlMergeMode(OVERRIDE)`. Consult the +xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[`@SqlMergeMode` annotation documentation section] +for examples and further details. 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..e269ea300e71 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 @@ -7,9 +7,10 @@ application context that you configured with `@ContextConfiguration` or related annotations. You may use setter injection, field injection, or both, depending on which annotations you choose and whether you place them on setter methods or fields. If you are using JUnit Jupiter you may also optionally use constructor injection -(see xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with `SpringExtension`]). For consistency with Spring's annotation-based -injection support, you may also use Spring's `@Autowired` annotation or the `@Inject` -annotation from JSR-330 for field and setter injection. +(see xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with `SpringExtension`]). +For consistency with Spring's annotation-based injection support, you may also use +Spring's `@Autowired` annotation or the `@Inject` annotation from JSR-330 for +field and setter injection. TIP: For testing frameworks other than JUnit Jupiter, the TestContext framework does not participate in instantiation of the test class. Thus, the use of `@Autowired` or @@ -35,9 +36,9 @@ dependency injection altogether by explicitly configuring your class with from the list of listeners. Consider the scenario of testing a `HibernateTitleRepository` class, as outlined in the -xref:testing/integration.adoc#integration-testing-goals[Goals] section. The next two code listings demonstrate the -use of `@Autowired` on fields and setter methods. The application context configuration -is presented after all sample code listings. +xref:testing/integration.adoc#integration-testing-goals[Goals] section. The next two code +listings demonstrate the use of `@Autowired` on fields and setter methods. The application +context configuration is presented after all sample code listings. [NOTE] ==== @@ -58,7 +59,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 +80,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 +107,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 +132,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 @@ -172,7 +173,7 @@ shows this configuration: - + @@ -192,7 +193,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 +208,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ... @@ -226,5 +227,3 @@ narrowing the set of type matches to a specific bean. Its value is matched again is used as a fallback qualifier value, so you can effectively also point to a specific bean by name there (as shown earlier, assuming that `myDataSource` is the bean `id`). ===== - - 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 6910ce06fb9e..ba6407d377b5 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 @@ -13,6 +13,7 @@ test execution by providing dependency injection, managing transactions, and so 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]] == `TestContext` @@ -21,6 +22,7 @@ actual testing framework in use) and provides context management and caching sup the test instance for which it is responsible. The `TestContext` also delegates to a `SmartContextLoader` to load an `ApplicationContext` if requested. + [[testcontextmanager]] == `TestContextManager` @@ -36,11 +38,14 @@ responsible for managing a single `TestContext` and signaling events to each reg * After any "`after`" or "`after each`" methods of a particular testing framework. * After any "`after class`" or "`after all`" methods of a particular testing framework. + [[testexecutionlistener]] == `TestExecutionListener` `TestExecutionListener` defines the API for reacting to test-execution events published by -the `TestContextManager` with which the listener is registered. See xref:testing/testcontext-framework/tel-config.adoc[`TestExecutionListener` Configuration]. +the `TestContextManager` with which the listener is registered. See +xref:testing/testcontext-framework/tel-config.adoc[`TestExecutionListener` Configuration]. + [[context-loaders]] == Context Loaders @@ -82,5 +87,3 @@ Spring provides the following implementations: locations. * `GenericXmlWebContextLoader`: Loads a `WebApplicationContext` from XML resource locations. - - 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 6cb00cbcc75f..9cfa44c145f4 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. @@ -44,5 +44,3 @@ the javadoc for {spring-framework-api}/test/context/TestContext.html[`TestContex `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..a98f0f72f9cf 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc @@ -1,190 +1,39 @@ [[testcontext-support-classes]] = TestContext Framework Support Classes -This section describes the various classes that support the Spring TestContext Framework. +This section describes the various classes that support the Spring TestContext Framework +in JUnit and TestNG. -[[testcontext-junit4-runner]] -== Spring JUnit 4 Runner - -The Spring TestContext Framework offers full integration with JUnit 4 through a custom -runner (supported on JUnit 4.12 or higher). By annotating test classes with -`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)` -variant, developers can implement standard JUnit 4-based unit and integration tests and -simultaneously reap the benefits of the TestContext framework, such as support for -loading application contexts, dependency injection of test instances, transactional test -method execution, and so on. If you want to use the Spring TestContext Framework with an -alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners -(such as the `MockitoJUnitRunner`), you can, optionally, use -xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules] instead. - -The following code listing shows the minimal requirements for configuring a test class to -run with the custom Spring `Runner`: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @RunWith(SpringRunner.class) - @TestExecutionListeners({}) - public class SimpleTest { - - @Test - public void testMethod() { - // test logic... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @RunWith(SpringRunner::class) - @TestExecutionListeners - class SimpleTest { - - @Test - fun testMethod() { - // test logic... - } - } ----- -====== - -In the preceding example, `@TestExecutionListeners` is configured with an empty list, to -disable the default listeners, which otherwise would require an `ApplicationContext` to -be configured through `@ContextConfiguration`. - -[[testcontext-junit4-rules]] -== Spring JUnit 4 Rules - -The `org.springframework.test.context.junit4.rules` package provides the following JUnit -4 rules (supported on JUnit 4.12 or higher): - -* `SpringClassRule` -* `SpringMethodRule` - -`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring -TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports -instance-level and method-level features of the Spring TestContext Framework. - -In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of -being independent of any `org.junit.runner.Runner` implementation and can, therefore, be -combined with existing alternative runners (such as JUnit 4's `Parameterized`) or -third-party runners (such as the `MockitoJUnitRunner`). - -To support the full functionality of the TestContext framework, you must combine a -`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way -to declare these rules in an integration test: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - // Optionally specify a non-Spring Runner via @RunWith(...) - @ContextConfiguration - public class IntegrationTest { - - @ClassRule - public static final SpringClassRule springClassRule = new SpringClassRule(); - - @Rule - public final SpringMethodRule springMethodRule = new SpringMethodRule(); - - @Test - public void testMethod() { - // test logic... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - // Optionally specify a non-Spring Runner via @RunWith(...) - @ContextConfiguration - class IntegrationTest { - - @Rule - val springMethodRule = SpringMethodRule() - - @Test - fun testMethod() { - // test logic... - } - - companion object { - @ClassRule - val springClassRule = SpringClassRule() - } - } ----- -====== - -[[testcontext-support-classes-junit4]] -== JUnit 4 Support Classes - -The `org.springframework.test.context.junit4` package provides the following support -classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher): - -* `AbstractJUnit4SpringContextTests` -* `AbstractTransactionalJUnit4SpringContextTests` - -`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the -Spring TestContext Framework with explicit `ApplicationContext` testing support in a -JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a -`protected` `applicationContext` instance variable that you can use to perform explicit -bean lookups or to test the state of the context as a whole. - -`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of -`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC -access. This class expects a `javax.sql.DataSource` bean and a -`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you -extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected` -`jdbcTemplate` instance variable that you can use to run SQL statements to query the -database. You can use such queries to confirm database state both before and after -running database-related application code, and Spring ensures that such queries run in -the scope of the same transaction as the application code. When used in conjunction with -an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives]. -As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support], -`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that -delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`. -Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an -`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`. - -TIP: These classes are a convenience for extension. If you do not want your test classes -to be tied to a Spring-specific class hierarchy, you can configure your own custom test -classes by using `@RunWith(SpringRunner.class)` or xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules] -. [[testcontext-junit-jupiter-extension]] == SpringExtension for JUnit Jupiter -The Spring TestContext Framework offers full integration with the JUnit Jupiter testing -framework, introduced in JUnit 5. By annotating test classes with -`@ExtendWith(SpringExtension.class)`, you can implement standard JUnit Jupiter-based unit -and integration tests and simultaneously reap the benefits of the TestContext framework, -such as support for loading application contexts, dependency injection of test instances, -transactional test method execution, and so on. +The `SpringExtension` integrates the Spring TestContext Framework into the JUnit Jupiter +testing framework. + +NOTE: As of Spring Framework 7.0, the `SpringExtension` requires JUnit Jupiter 6.0 or higher. + +By annotating test classes with `@ExtendWith(SpringExtension.class)`, you can implement +standard JUnit Jupiter-based unit and integration tests and simultaneously reap the +benefits of the TestContext framework, such as support for loading application contexts, +dependency injection of test instances, transactional test method execution, and so on. Furthermore, thanks to the rich extension API in JUnit Jupiter, Spring provides the following features above and beyond the feature set that Spring supports for JUnit 4 and TestNG: * Dependency injection for test constructors, test methods, and test lifecycle callback - methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with the `SpringExtension`] for further details. -* Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional + methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency + Injection with the `SpringExtension`] for further details. +* Powerful support for link:https://docs.junit.org/current/extensions/conditional-test-execution.html[conditional test execution] based on SpEL expressions, environment variables, system properties, and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in - xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details and examples. + xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] + for further details and examples. * Custom composed annotations that combine annotations from Spring and JUnit Jupiter. See the `@TransactionalDevTestConfig` and `@TransactionalIntegrationTest` examples in - xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for further details. + xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for + further details. The following code listing shows how to configure a test class to use the `SpringExtension` in conjunction with `@ContextConfiguration`: @@ -193,7 +42,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 +59,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) @@ -226,8 +75,8 @@ Kotlin:: ---- ====== -Since you can also use annotations in JUnit 5 as meta-annotations, Spring provides the -`@SpringJUnitConfig` and `@SpringJUnitWebConfig` composed annotations to simplify the +Since you can also use annotations in JUnit Jupiter as meta-annotations, Spring provides +the `@SpringJUnitConfig` and `@SpringJUnitWebConfig` composed annotations to simplify the configuration of the test `ApplicationContext` and JUnit Jupiter. The following example uses `@SpringJUnitConfig` to reduce the amount of configuration @@ -237,7 +86,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 +102,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 +124,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 +140,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 @@ -307,22 +156,21 @@ Kotlin:: ====== See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in -xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details. +xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] +for further details. [[testcontext-junit-jupiter-di]] === Dependency Injection with the `SpringExtension` The `SpringExtension` implements the -link:https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`] +link:https://docs.junit.org/current/extensions/parameter-resolution.html[`ParameterResolver`] extension API from JUnit Jupiter, which lets Spring provide dependency injection for test constructors, test methods, and test lifecycle callback methods. Specifically, the `SpringExtension` can inject dependencies from the test's -`ApplicationContext` into test constructors and methods that are annotated with -Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, -`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, -and others. - +`ApplicationContext` into test constructors and methods that are annotated with Spring's +`@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, `@AfterAll`, +`@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and others. [[testcontext-junit-jupiter-di-constructor]] ==== Constructor Injection @@ -331,6 +179,10 @@ If a specific parameter in a constructor for a JUnit Jupiter test class is of ty `ApplicationContext` (or a sub-type thereof) or is annotated or meta-annotated with `@Autowired`, `@Qualifier`, or `@Value`, Spring injects the value for that specific parameter with the corresponding bean or value from the test's `ApplicationContext`. +Similarly, if a specific parameter is annotated with `@MockitoBean` or `@MockitoSpyBean`, +Spring will inject a Mockito mock or spy, respectively — see +xref:testing/annotations/integration-spring/annotation-mockitobean.adoc[`@MockitoBean` and `@MockitoSpyBean`] +for details. Spring can also be configured to autowire all arguments for a test class constructor if the constructor is considered to be _autowirable_. A constructor is considered to be @@ -341,8 +193,9 @@ autowirable if one of the following conditions is met (in order of precedence). attribute set to `ALL`. * The default _test constructor autowire mode_ has been changed to `ALL`. -See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`] for details on the use of -`@TestConstructor` and how to change the global _test constructor autowire mode_. +See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`] +for details on the use of `@TestConstructor` and how to change the global _test +constructor autowire mode_. WARNING: If the constructor for a test class is considered to be _autowirable_, Spring assumes the responsibility for resolving arguments for all parameters in the constructor. @@ -376,7 +229,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 +247,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){ @@ -407,14 +260,15 @@ Kotlin:: Note that this feature lets test dependencies be `final` and therefore immutable. If the `spring.test.constructor.autowire.mode` property is to `all` (see -xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]), we can omit the declaration of -`@Autowired` on the constructor in the previous example, resulting in the following. +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]), +we can omit the declaration of `@Autowired` on the constructor in the previous example, +resulting in the following. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) class OrderServiceIntegrationTests { @@ -431,7 +285,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 +309,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 +323,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 +347,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 +364,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 +385,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,26 +396,46 @@ 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. +[NOTE] +==== +As of Spring Framework 7.0, the `SpringExtension` uses a test-method scoped +`ExtensionContext` within `@Nested` test class hierarchies by default. However, the +`SpringExtension` can be configured to use a test-class scoped `ExtensionContext` — for +example via `@SpringExtensionConfig` or the `spring.test.extension.context.scope` Spring +property (see +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`]). +==== + +[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 -classpath. See the xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"] - note for details. +classpath. See the +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"] +note for details. Although the following "Hello World" example is very simplistic, it shows how to declare common configuration on a top-level class that is inherited by its `@Nested` test classes. In this particular example, only the `TestConfig` configuration class is inherited. Each nested test class provides its own set of active profiles, resulting in a distinct `ApplicationContext` for each nested test class (see -xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details). Consult the list of -xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations] to see -which annotations can be inherited in `@Nested` test classes. +xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details). +Consult the list of +xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations] +to see which annotations can be inherited in `@Nested` test classes. [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig.class) class GreetingServiceTests { @@ -594,7 +464,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) class GreetingServiceTests { @@ -622,8 +492,198 @@ Kotlin:: ---- ====== + +[[testcontext-junit4-support]] +== JUnit 4 Support + +[[testcontext-junit4-runner]] +=== Spring JUnit 4 Runner + +[WARNING] +==== +JUnit 4 is officially in maintenance mode, and JUnit 4 support in Spring is deprecated +since Spring Framework 7.0 in favor of the +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and JUnit Jupiter. +==== + +The Spring TestContext Framework offers full integration with JUnit 4 through a custom +runner (supported on JUnit 4.12 or higher). By annotating test classes with +`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)` +variant, developers can implement standard JUnit 4-based unit and integration tests and +simultaneously reap the benefits of the TestContext framework, such as support for +loading application contexts, dependency injection of test instances, transactional test +method execution, and so on. If you want to use the Spring TestContext Framework with an +alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners +(such as the `MockitoJUnitRunner`), you can, optionally, use +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules] +instead. + +The following code listing shows the minimal requirements for configuring a test class to +run with the custom Spring `Runner`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @RunWith(SpringRunner.class) + @TestExecutionListeners({}) + public class SimpleTest { + + @Test + public void testMethod() { + // test logic... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @RunWith(SpringRunner::class) + @TestExecutionListeners + class SimpleTest { + + @Test + fun testMethod() { + // test logic... + } + } +---- +====== + +In the preceding example, `@TestExecutionListeners` is configured with an empty list, to +disable the default listeners, which otherwise would require an `ApplicationContext` to +be configured through `@ContextConfiguration`. + +[[testcontext-junit4-rules]] +=== Spring JUnit 4 Rules + +[WARNING] +==== +JUnit 4 is officially in maintenance mode, and JUnit 4 support in Spring is deprecated +since Spring Framework 7.0 in favor of the +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and JUnit Jupiter. +==== + +The `org.springframework.test.context.junit4.rules` package provides the following JUnit +4 rules (supported on JUnit 4.12 or higher): + +* `SpringClassRule` +* `SpringMethodRule` + +`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring +TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports +instance-level and method-level features of the Spring TestContext Framework. + +In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of +being independent of any `org.junit.runner.Runner` implementation and can, therefore, be +combined with existing alternative runners (such as JUnit 4's `Parameterized`) or +third-party runners (such as the `MockitoJUnitRunner`). + +To support the full functionality of the TestContext framework, you must combine a +`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way +to declare these rules in an integration test: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + // Optionally specify a non-Spring Runner via @RunWith(...) + @ContextConfiguration + public class IntegrationTest { + + @ClassRule + public static final SpringClassRule springClassRule = new SpringClassRule(); + + @Rule + public final SpringMethodRule springMethodRule = new SpringMethodRule(); + + @Test + public void testMethod() { + // test logic... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + // Optionally specify a non-Spring Runner via @RunWith(...) + @ContextConfiguration + class IntegrationTest { + + @Rule + val springMethodRule = SpringMethodRule() + + @Test + fun testMethod() { + // test logic... + } + + companion object { + @ClassRule + val springClassRule = SpringClassRule() + } + } +---- +====== + +[[testcontext-support-classes-junit4]] +=== JUnit 4 Base Classes + +[WARNING] +==== +JUnit 4 is officially in maintenance mode, and JUnit 4 support in Spring is deprecated +since Spring Framework 7.0 in favor of the +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +and JUnit Jupiter. +==== + +The `org.springframework.test.context.junit4` package provides the following support +classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher): + +* `AbstractJUnit4SpringContextTests` +* `AbstractTransactionalJUnit4SpringContextTests` + +`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the +Spring TestContext Framework with explicit `ApplicationContext` testing support in a +JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a +`protected` `applicationContext` instance variable that you can use to perform explicit +bean lookups or to test the state of the context as a whole. + +`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of +`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC +access. This class expects a `javax.sql.DataSource` bean and a +`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you +extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected` +`jdbcTemplate` instance variable that you can use to run SQL statements to query the +database. You can use such queries to confirm database state both before and after +running database-related application code, and Spring ensures that such queries run in +the scope of the same transaction as the application code. When used in conjunction with +an ORM tool, be sure to avoid +xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives]. +As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support], +`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that +delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`. +Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an +`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`. + +TIP: These classes are a convenience for extension. If you do not want your test classes +to be tied to a Spring-specific class hierarchy, you can configure your own custom test +classes by using `@RunWith(SpringRunner.class)` or +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules]. + + [[testcontext-support-classes-testng]] -== TestNG Support Classes +== TestNG Support The `org.springframework.test.context.testng` package provides the following support classes for TestNG based test cases: @@ -646,7 +706,8 @@ extend `AbstractTransactionalTestNGSpringContextTests`, you can access a `protec database. You can use such queries to confirm database state both before and after running database-related application code, and Spring ensures that such queries run in the scope of the same transaction as the application code. When used in conjunction with -an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives]. +an ORM tool, be sure to avoid +xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives]. As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support], `AbstractTransactionalTestNGSpringContextTests` also provides convenience methods that delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`. @@ -658,4 +719,3 @@ to be tied to a Spring-specific class hierarchy, you can configure your own cust classes by using `@ContextConfiguration`, `@TestExecutionListeners`, and so on and by manually instrumenting your test class with a `TestContextManager`. See the source code of `AbstractTestNGSpringContextTests` for an example of how to instrument your test class. - 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 97bdec9d4878..0b89b0672419 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,15 +7,17 @@ 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 @@ -24,6 +26,8 @@ by default, exactly in the following order: 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 @@ -45,7 +49,7 @@ following. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // Switch to default listeners @TestExecutionListeners( @@ -59,7 +63,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // Switch to default listeners @TestExecutionListeners( @@ -73,6 +77,7 @@ Kotlin:: ====== ==== + [[testcontext-tel-config-automatic-discovery]] == Automatic Discovery of Default `TestExecutionListener` Implementations @@ -89,6 +94,7 @@ 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 @@ -104,6 +110,7 @@ by implementing `Ordered` or declaring `@Order`. See the javadoc for the `getOrd methods of the core default `TestExecutionListener` implementations for details on what values are assigned to each core listener. + [[testcontext-tel-config-merging]] == Merging `TestExecutionListener` Implementations @@ -116,7 +123,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({ @@ -135,7 +142,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestExecutionListeners( @@ -159,15 +166,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. @@ -183,7 +191,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( @@ -197,7 +205,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @ContextConfiguration @TestExecutionListeners( 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..90f2fee4a74c 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. @@ -66,6 +66,7 @@ package. * `@AfterTestMethod` * `@AfterTestClass` + [[testcontext-test-execution-events-exception-handling]] == Exception Handling @@ -77,12 +78,11 @@ contrast, if an asynchronous test execution event listener throws an exception, exception will not propagate to the underlying testing framework. For further details on asynchronous exception handling, consult the class-level javadoc for `@EventListener`. + [[testcontext-test-execution-events-async]] == Asynchronous Listeners If you want a particular test execution event listener to process events asynchronously, -you can use Spring's xref:integration/scheduling.adoc#scheduling-annotation-support-async[regular `@Async` support] -. For further details, consult the class-level javadoc for -`@EventListener`. - - +you can use Spring's +xref:integration/scheduling.adoc#scheduling-annotation-support-async[regular `@Async` support]. +For further details, consult the class-level javadoc for `@EventListener`. 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 f34ad15f9457..e3df72feb173 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc @@ -9,6 +9,7 @@ transactions, however, you must configure a `PlatformTransactionManager` bean in details are provided later). In addition, you must declare Spring's `@Transactional` annotation either at the class or the method level for your tests. + [[testcontext-tx-test-managed-transactions]] == Test-managed Transactions @@ -46,6 +47,7 @@ Situations in which this can occur include but are not limited to the following. * TestNG's `@Test(timeOut = ...)` support ==== + [[testcontext-tx-enabling-transactions]] == Enabling and Disabling Transactions @@ -106,7 +108,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 +152,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @SpringJUnitConfig(TestConfig::class) @Transactional @@ -193,9 +195,11 @@ Kotlin:: ---- ====== -As explained in xref:testing/testcontext-framework/tx.adoc#testcontext-tx-rollback-and-commit-behavior[Transaction Rollback and Commit Behavior], there is no need to -clean up the database after the `createUser()` method runs, since any changes made to the -database are automatically rolled back by the `TransactionalTestExecutionListener`. +As explained in xref:testing/testcontext-framework/tx.adoc#testcontext-tx-rollback-and-commit-behavior[Transaction Rollback and Commit Behavior], +there is no need to clean up the database after the `createUser()` method runs, +since any changes made to the database are automatically rolled back by the +`TransactionalTestExecutionListener`. + [[testcontext-tx-rollback-and-commit-behavior]] == Transaction Rollback and Commit Behavior @@ -205,6 +209,7 @@ test; however, transactional commit and rollback behavior can be configured decl via the `@Commit` and `@Rollback` annotations. See the corresponding entries in the xref:testing/annotations.adoc[annotation support] section for further details. + [[testcontext-tx-programmatic-tx-mgt]] == Programmatic Transaction Management @@ -223,7 +228,7 @@ for further details. ====== 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 +260,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() { @@ -285,6 +290,7 @@ Kotlin:: ---- ====== + [[testcontext-tx-before-and-after-tx]] == Running Code Outside of a Transaction @@ -303,7 +309,7 @@ before-transaction method or after-transaction method runs at the appropriate ti Generally speaking, `@BeforeTransaction` and `@AfterTransaction` methods must not accept any arguments. -However, as of Spring Framework 6.1, for tests using the +However, for tests using the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] with JUnit Jupiter, `@BeforeTransaction` and `@AfterTransaction` methods may optionally accept arguments which will be resolved by any registered JUnit `ParameterResolver` @@ -316,7 +322,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 +332,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) { @@ -346,6 +352,7 @@ Similarly, methods annotated with `@BeforeTransaction` or `@AfterTransaction` ar run for transactional test methods. ==== + [[testcontext-tx-mgr-config]] == Configuring a Transaction Manager @@ -355,10 +362,11 @@ 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 -{spring-framework-api}/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`. + [[testcontext-tx-annotation-demo]] == Demonstration of All Transaction-related Annotations @@ -375,7 +383,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 +422,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 +477,7 @@ session: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // ... @@ -497,7 +505,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ... @@ -530,7 +538,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 +566,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ... @@ -612,7 +620,7 @@ example. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- // ... @@ -640,7 +648,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // ... @@ -671,5 +679,3 @@ See {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..61806e0676e2 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 { @@ -166,4 +166,3 @@ Kotlin:: } ---- ====== - diff --git a/framework-docs/modules/ROOT/pages/testing/unit.adoc b/framework-docs/modules/ROOT/pages/testing/unit.adoc index d2ce8648b0e9..6e96deb6da25 100644 --- a/framework-docs/modules/ROOT/pages/testing/unit.adoc +++ b/framework-docs/modules/ROOT/pages/testing/unit.adoc @@ -19,18 +19,15 @@ however, the Spring Framework provides mock objects and testing support classes, are described in this chapter. - [[mock-objects]] == Mock Objects 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] - [[mock-objects-env]] === Environment @@ -41,36 +38,19 @@ and xref:core/beans/environment.adoc#beans-property-source-abstraction[`Property `MockEnvironment` and `MockPropertySource` are useful for developing 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]). - -TIP: Since Spring Framework 6.0, the mock objects in `org.springframework.mock.web` are -based on the Servlet 6.0 API. +convenient to use than dynamic mock objects (such as https://easymock.org/[EasyMock]). -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]. +TIP: Since Spring Framework 7.0, the mock objects in `org.springframework.mock.web` are +based on the Servlet 6.1 API. +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]] === Spring Web Reactive @@ -95,7 +75,6 @@ testing WebFlux applications without an HTTP server. The client can also be used end-to-end tests with a running server. - [[unit-testing-support-classes]] == Unit Testing Support Classes @@ -105,7 +84,6 @@ categories: * xref:testing/unit.adoc#unit-testing-utilities[General Testing Utilities] * xref:testing/unit.adoc#unit-testing-spring-mvc[Spring MVC Testing Utilities] - [[unit-testing-utilities]] === General Testing Utilities @@ -121,6 +99,11 @@ mock to configure expectations on it and perform verifications. For Spring's cor utilities, see {spring-framework-api}/aop/support/AopUtils.html[`AopUtils`] and {spring-framework-api}/aop/framework/AopProxyUtils.html[`AopProxyUtils`]. +TIP: For guidance on using `AopTestUtils` together with `@MockitoSpyBean` when the spied +bean is wrapped in a Spring AOP proxy, see +xref:testing/annotations/integration-spring/annotation-mockitobean.adoc#spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies[`@MockitoSpyBean` +and Spring AOP Proxies]. + {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, @@ -150,7 +133,6 @@ assigned by the operating system. To interact with that server, you should query server for the port it is currently using. ==== - [[unit-testing-spring-mvc]] === Spring MVC Testing Utilities @@ -162,7 +144,7 @@ that deal with Spring MVC `ModelAndView` objects. .Unit testing Spring MVC Controllers TIP: To unit test your Spring MVC `Controller` classes as POJOs, use `ModelAndViewAssert` combined with `MockHttpServletRequest`, `MockHttpSession`, and so on from Spring's -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/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 +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 f840531788ad..b2bd9807be21 100644 --- a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc +++ b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc @@ -8,16 +8,12 @@ perform end-to-end HTTP tests. It can also be used to test Spring MVC and Spring applications without a running server via mock server request and response objects. - - [[webtestclient-setup]] == Setup To set up a `WebTestClient` you need to choose a server setup to bind to. This can be one of several mock server setup choices or a connection to a live server. - - [[webtestclient-controller-config]] === Bind to Controller @@ -33,7 +29,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(); @@ -41,7 +37,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = WebTestClient.bindToController(TestController()).build() ---- @@ -51,13 +47,13 @@ For Spring MVC, use the following which delegates to the {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(); @@ -65,14 +61,12 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val client = MockMvcWebTestClient.bindToController(TestController()).build() ---- ====== - - [[webtestclient-context-config]] === Bind to `ApplicationContext` @@ -81,7 +75,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 -{spring-framework-api}/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: @@ -89,7 +83,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 { @@ -108,7 +102,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 { @@ -127,15 +121,15 @@ Kotlin:: ====== For Spring MVC, use the following where the Spring `ApplicationContext` is passed to -{spring-framework-api}/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> @@ -162,7 +156,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> @@ -188,12 +182,10 @@ Kotlin:: <3> Create the `WebTestClient` ====== - - [[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 @@ -203,7 +195,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(); @@ -211,7 +203,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() @@ -221,8 +213,6 @@ Kotlin:: For Spring MVC there are currently no options to test xref:web/webmvc-functional.adoc[WebMvc functional endpoints]. - - [[webtestclient-server-config]] === Bind to Server @@ -232,21 +222,19 @@ 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() ---- ====== - - [[webtestclient-client-config]] === Client Config @@ -260,21 +248,23 @@ follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- client = WebTestClient.bindToController(new TestController()) .configureClient() .baseUrl("/test") + .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) .build(); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client = WebTestClient.bindToController(TestController()) .configureClient() .baseUrl("/test") + .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) .build() ---- ====== @@ -285,13 +275,21 @@ Kotlin:: [[webtestclient-tests]] == Writing Tests -`WebTestClient` provides an API identical to xref:web/webflux-webclient.adoc[WebClient] -up to the point of performing a request by using `exchange()`. See the -xref:web/webflux-webclient/client-body.adoc[WebClient] documentation for examples on how to -prepare a request with any content including form data, multipart data, and more. +xref:web/webflux-webclient.adoc[WebClient] and `WebTestClient` have +the same API up to the point of the call to `exchange()`. After that, `WebTestClient` +provides two alternative ways to verify the response: + +1. xref:webtestclient-workflow[Built-in Assertions] extend the request workflow with a chain of expectations +2. xref:webtestclient-assertj[AssertJ Integration] to verify the response via `assertThat()` statements -After the call to `exchange()`, `WebTestClient` diverges from the `WebClient` and -instead continues with a workflow to verify responses. +TIP: See the xref:web/webflux-webclient/client-body.adoc[WebClient] documentation for +examples on how to prepare a request with any content including form data, +multipart data, and more. + + + +[[webtestclient-workflow]] +=== Built-in Assertions To assert the response status and headers, use the following: @@ -299,7 +297,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) @@ -310,7 +308,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) @@ -329,7 +327,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) @@ -339,6 +337,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: @@ -353,7 +364,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() @@ -363,7 +374,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 @@ -381,29 +392,29 @@ 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 + import org.springframework.test.web.reactive.server.expectBody client.get().uri("/persons/1") .exchange() .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)... } ---- ====== @@ -414,7 +425,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() @@ -425,7 +436,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 @@ -445,7 +456,7 @@ instead of `Class`. [[webtestclient-no-content]] -=== No Content +==== No Content If the response is not expected to have content, you can assert that as follows: @@ -453,7 +464,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) @@ -464,7 +475,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) @@ -481,7 +492,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() @@ -491,7 +502,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() @@ -503,7 +514,7 @@ Kotlin:: [[webtestclient-json]] -=== JSON Content +==== JSON Content You can use `expectBody()` without a target type to perform assertions on the raw content rather than through higher level Object(s). @@ -514,7 +525,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() @@ -525,7 +536,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() @@ -541,7 +552,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() @@ -553,7 +564,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.get().uri("/persons") .exchange() @@ -567,17 +578,17 @@ Kotlin:: [[webtestclient-stream]] -=== Streaming Responses +==== Streaming Responses -To test potentially infinite streams such as `"text/event-stream"` or -`"application/x-ndjson"`, start by verifying the response status and headers, and then +To test potentially infinite streams such as `"text/event-stream"`, +`"application/jsonl"` or `"application/x-ndjson"`, start by verifying the response status and headers, and then obtain a `FluxExchangeResult`: [tabs] ====== 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) @@ -589,7 +600,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 @@ -607,7 +618,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(); @@ -621,7 +632,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val eventFlux = result.getResponseBody() @@ -635,6 +646,77 @@ Kotlin:: ====== + +[[webtestclient-assertj]] +=== AssertJ Integration + +`WebTestClientResponse` is the main entry point for the AssertJ integration. +It is an `AssertProvider` that wraps the `ResponseSpec` of an exchange in order to enable +use of `assertThat()` statements. For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ResponseSpec spec = client.get().uri("/persons").exchange(); + + WebTestClientResponse response = WebTestClientResponse.from(spec); + assertThat(response).hasStatusOk(); + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); + // ... +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val spec = client.get().uri("/persons").exchange() + + val response = WebTestClientResponse.from(spec) + assertThat(response).hasStatusOk() + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) + // ... +---- +====== + +You can also use the built-in workflow first, and then obtain an `ExchangeResult` to wrap +and continue with AssertJ. For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ExchangeResult result = client.get().uri("/persons").exchange() + . // ... + .returnResult(); + + WebTestClientResponse response = WebTestClientResponse.from(result); + assertThat(response).hasStatusOk(); + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); + // ... +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val result = client.get().uri("/persons").exchange() + . // ... + .returnResult() + + val response = WebTestClientResponse.from(spec) + assertThat(response).hasStatusOk() + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) + // ... +---- +====== + + + [[webtestclient-mockmvc]] === MockMvc Assertions @@ -649,7 +731,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") @@ -666,7 +748,7 @@ 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") @@ -688,7 +770,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)) @@ -697,11 +779,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 378d65ef5911..eea40b37309f 100644 --- a/framework-docs/modules/ROOT/pages/web-reactive.adoc +++ b/framework-docs/modules/ROOT/pages/web-reactive.adoc @@ -2,11 +2,11 @@ = Web on Reactive Stack This part of the documentation covers support for reactive-stack web applications built -on a {reactive-streams-site}/[Reactive Streams] API to run on non-blocking -servers, such as Netty, Undertow, and Servlet containers. Individual chapters cover +on a {reactive-streams-site}/[Reactive Streams] API to run on non-blocking servers, +such as Netty 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/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]. +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 e8a18f927c88..d579e65f4761 100644 --- a/framework-docs/modules/ROOT/pages/web.adoc +++ b/framework-docs/modules/ROOT/pages/web.adoc @@ -2,8 +2,11 @@ = Web on Servlet Stack :page-section-summary-toc: 1 -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:web-reactive.adoc[Web on Reactive Stack]. +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: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 deleted file mode 100644 index 55276ff2bcb2..000000000000 --- a/framework-docs/modules/ROOT/pages/web/integration.adoc +++ /dev/null @@ -1,199 +0,0 @@ -[[web-integration]] -= Other Web Frameworks - -This chapter details Spring's integration with third-party web frameworks. - -One of the core value propositions of the Spring Framework is that of enabling -_choice_. In a general sense, Spring does not force you to use or buy into any -particular architecture, technology, or methodology (although it certainly recommends -some over others). This freedom to pick and choose the architecture, technology, or -methodology that is most relevant to a developer and their development team is -arguably most evident in the web area, where Spring provides its own web frameworks -(xref:web/webmvc.adoc#mvc[Spring MVC] and xref:web/webflux.adoc#webflux[Spring WebFlux]) while, at the same time, -supporting integration with a number of popular third-party web frameworks. - - - - -[[web-integration-common]] -== Common Configuration - -Before diving into the integration specifics of each supported web framework, let us -first take a look at common Spring configuration that is not specific to any one web -framework. (This section is equally applicable to Spring's own web framework variants.) - -One of the concepts (for want of a better word) espoused by Spring's lightweight -application model is that of a layered architecture. Remember that in a "classic" -layered architecture, the web layer is but one of many layers. It serves as one of the -entry points into a server-side application, and it delegates to service objects -(facades) that are defined in a service layer to satisfy business-specific (and -presentation-technology agnostic) use cases. In Spring, these service objects, any other -business-specific objects, data-access objects, and others exist in a distinct "business -context", which contains no web or presentation layer objects (presentation objects, -such as Spring MVC controllers, are typically configured in a distinct "presentation -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 -{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. - -Consider the following `` configuration: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - org.springframework.web.context.ContextLoaderListener - ----- - -Further consider the following `` configuration: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - contextConfigLocation - /WEB-INF/applicationContext*.xml - ----- - -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 -{spring-framework-api}/web/context/WebApplicationContext.html[`WebApplicationContext`] -object based on the bean definitions and stores it in the `ServletContext` of the web -application. - -All Java web frameworks are built on top of the Servlet API, so you can use the -following code snippet to get access to this "business context" `ApplicationContext` -created by the `ContextLoaderListener`. - -The following example shows how to get the `WebApplicationContext`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext); ----- - -The -{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` -key. Rather than risk getting `NullPointerExceptions` in your application, it is better -to use the `getRequiredWebApplicationContext()` method. This method throws an exception -when the `ApplicationContext` is missing. - -Once you have a reference to the `WebApplicationContext`, you can retrieve beans by their -name or type. Most developers retrieve beans by name and then cast them to one of their -implemented interfaces. - -Fortunately, most of the frameworks in this section have simpler ways of looking up beans. -Not only do they make it easy to get beans from a Spring container, but they also let you -use dependency injection on their controllers. Each web framework section has more detail -on its specific integration strategies. - - - - -[[jsf]] -== JSF - -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. - -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 -environment. Spring's JSF support is not actively evolved anymore and primarily -exists for migration purposes when modernizing older JSF-based applications. - -The key element in Spring's JSF integration is the JSF `ELResolver` mechanism. - - - -[[jsf-springbeanfaceselresolver]] -=== Spring Bean Resolver - -`SpringBeanFacesELResolver` is a JSF compliant `ELResolver` implementation, -integrating with the standard Unified EL as used by JSF and JSP. It delegates to -Spring's "business context" `WebApplicationContext` first and then to the -default resolver of the underlying JSF implementation. - -Configuration-wise, you can define `SpringBeanFacesELResolver` in your JSF -`faces-context.xml` file, as the following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - org.springframework.web.jsf.el.SpringBeanFacesELResolver - ... - - ----- - - - -[[jsf-facescontextutils]] -=== Using `FacesContextUtils` - -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 {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. - -The following example shows how to use `FacesContextUtils`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance()); ----- - - - - -[[struts]] -== Apache Struts - -Invented by Craig McClanahan, https://struts.apache.org[Struts] is an open-source project -hosted by the Apache Software Foundation. Struts 1.x greatly simplified the -JSP/Servlet programming paradigm and won over many developers who were using proprietary -frameworks. It simplified the programming model; it was open source; and it had a large -community, which let the project grow and become popular among Java web developers. - -As a successor to the original Struts 1.x, check out Struts 2.x or more recent versions -as well as the Struts-provided -https://struts.apache.org/plugins/spring/[Spring Plugin] for built-in Spring integration. - - - - -[[tapestry]] -== Apache Tapestry - -https://tapestry.apache.org/[Tapestry] is a "Component oriented framework for creating -dynamic, robust, highly scalable web applications in Java." - -While Spring has its own xref:web/webmvc.adoc#mvc[powerful web layer], there are a number of unique -advantages to building an enterprise Java application by using a combination of Tapestry -for the web user interface and the Spring container for the lower layers. - -For more information, see Tapestry's dedicated -https://tapestry.apache.org/integrating-with-spring-framework.html[integration module for Spring]. - - - - -[[web-integration-resources]] -== Further Resources - -The following links go to further resources about the various web frameworks described in -this chapter. - -* The https://www.oracle.com/java/technologies/javaserverfaces.html[JSF] homepage -* The https://struts.apache.org/[Struts] homepage -* The https://tapestry.apache.org/[Tapestry] homepage diff --git a/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc b/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc index 4de277efa7ea..dc2c136ecc10 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-cors.adoc @@ -1,13 +1,12 @@ [[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 describes how to do so. - - [[webflux-cors-intro]] == Introduction [.small]#xref:web/webmvc-cors.adoc#mvc-cors-intro[See equivalent in the Servlet stack]# @@ -23,8 +22,6 @@ what kind of cross-domain requests are authorized, rather than using less secure powerful workarounds based on IFRAME or JSONP. - - [[webflux-cors-processing]] == Processing [.small]#xref:web/webmvc-cors.adoc#mvc-cors-processing[See equivalent in the Servlet stack]# @@ -42,12 +39,12 @@ required CORS response headers set. In order to enable cross-origin requests (that is, the `Origin` header is present and differs from the host of the request), you need to have some explicitly declared CORS -configuration. If no matching CORS configuration is found, preflight requests are -rejected. No CORS headers are added to the responses of simple and actual CORS requests -and, consequently, browsers reject them. +configuration. If no matching CORS configuration is found, no CORS headers are added to +the responses to preflight, simple and actual CORS requests and, consequently, browsers +reject them. Each `HandlerMapping` can be -{spring-framework-api}/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 +57,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 -{spring-framework-api}/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] @@ -73,8 +70,6 @@ To learn more from the source or to make advanced customizations, see: ==== - - [[webflux-cors-credentialed-requests]] == Credentialed Requests [.small]#xref:web/webmvc-cors.adoc#mvc-cors-credentialed-requests[See equivalent in the Servlet stack]# @@ -102,8 +97,6 @@ WARNING: While such wildcard configuration can be handy, it is recommended when a finite set of values instead to provide a higher level of security. - - [[webflux-cors-controller]] == `@CrossOrigin` [.small]#xref:web/webmvc-cors.adoc#mvc-cors-controller[See equivalent in the Servlet stack]# @@ -117,7 +110,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 +131,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/account") @@ -181,7 +174,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 +195,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 +224,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 +248,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin(maxAge = 3600) // <1> @RestController @@ -279,8 +272,6 @@ Kotlin:: ====== -- - - [[webflux-cors-global]] == Global Configuration [.small]#xref:web/webmvc-cors.adoc#mvc-cors-global[See equivalent in the Servlet stack]# @@ -311,10 +302,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 @@ -334,10 +324,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 addCorsMappings(registry: CorsRegistry) { @@ -356,15 +345,13 @@ Kotlin:: ====== - - [[webflux-cors-webfilter]] == CORS `WebFilter` [.small]#xref:web/webmvc-cors.adoc#mvc-cors-filter[See equivalent in the Servlet stack]# You can apply CORS support through the built-in {spring-framework-api}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a -good fit with <>. +good fit with xref:web/webflux-functional.adoc[functional endpoints]. NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for @@ -377,7 +364,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 +388,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 3966294c90de..5a9308254db8 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 @@ -8,15 +9,13 @@ It is an alternative to the annotation-based programming model but otherwise run the same xref:web/webflux/reactive-spring.adoc[Reactive Core] foundation. - - [[webflux-fn-overview]] == Overview [.small]#xref:web/webmvc-functional.adoc#webmvc-fn-overview[See equivalent in the Servlet stack]# -In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes +In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes a `ServerRequest` and returns a delayed `ServerResponse` (i.e. `Mono`). -Both the request and the response object have immutable contracts that offer JDK 8-friendly +Both the request and the response object have immutable contracts that offer convenient access to the HTTP request and response. `HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the annotation-based programming model. @@ -34,7 +33,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 +70,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) @@ -114,21 +113,17 @@ through one of the built-in xref:web/webflux/reactive-spring.adoc#webflux-httpha Most applications can run through the WebFlux Java configuration, see xref:web/webflux-functional.adoc#webflux-fn-running[Running a Server]. - - [[webflux-fn-handler-functions]] == HandlerFunction [.small]#xref:web/webmvc-functional.adoc#webmvc-fn-handler-functions[See equivalent in the Servlet stack]# -`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly +`ServerRequest` and `ServerResponse` are immutable interfaces that offer convenient access to the HTTP request and response. 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`. -For more on that, see xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries]. - - +For more on that, see xref:web/webflux-reactive-libraries.adoc[Reactive Libraries]. [[webflux-fn-request]] === ServerRequest @@ -142,14 +137,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 +158,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 +180,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 +188,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 +201,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 +220,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() ---- @@ -240,66 +235,14 @@ val map = request.awaitMultipartData() The following example shows how to access multipart data, one at a time, in streaming fashion: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- -Flux allPartEvents = request.bodyToFlux(PartEvent.class); -allPartsEvents.windowUntil(PartEvent::isLast) - .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { - if (signal.hasValue()) { - PartEvent event = signal.get(); - if (event instanceof FormPartEvent formEvent) { - String value = formEvent.value(); - // handle form field - } - else if (event instanceof FilePartEvent fileEvent) { - String filename = fileEvent.filename(); - Flux contents = partEvents.map(PartEvent::content); - // handle file upload - } - else { - return Mono.error(new RuntimeException("Unexpected event: " + event)); - } - } - else { - return partEvents; // either complete or error signal - } - })); ----- +include-code::./PartEventHandler[tag=snippet,indent=0] -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- -val parts = request.bodyToFlux() -allPartsEvents.windowUntil(PartEvent::isLast) - .concatMap { - it.switchOnFirst { signal, partEvents -> - if (signal.hasValue()) { - val event = signal.get() - if (event is FormPartEvent) { - val value: String = event.value(); - // handle form field - } else if (event is FilePartEvent) { - val filename: String = event.filename(); - val contents: Flux = partEvents.map(PartEvent::content); - // handle file upload - } else { - return Mono.error(RuntimeException("Unexpected event: " + event)); - } - } else { - return partEvents; // either complete or error signal - } - } - } -} ----- -====== +NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks. + +The following shows how to bind request parameters, URI variables, or headers via `DataBinder`, +and also shows how to customize the `DataBinder`: -Note that the body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks. +include-code::./RequestHandler[tag=snippet,indent=0] [[webflux-fn-response]] === ServerResponse @@ -309,24 +252,7 @@ a `build` method to create it. You can use the builder to set the response statu headers, or to provide a body. The following example creates a 200 (OK) response with JSON content: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -Mono person = ... -ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class); ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -val person: Person = ... -ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person) ----- -====== +include-code::./ResponseHandler[tag=snippet,indent=0] The following example shows how to build a 201 (CREATED) response with a `Location` header and no body: @@ -334,18 +260,18 @@ 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(); +return ServerResponse.created(location).build(); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val location: URI = ... -ServerResponse.created(location).build() +return ServerResponse.created(location).build() ---- ====== @@ -356,20 +282,19 @@ body is serialized or deserialized. For example, to specify a {baeldung-blog}/ja ====== Java:: + -[source,java,role="primary"] +[source,java] ---- -ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...); +return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...); ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- -ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...) +return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...) ---- ====== - [[webflux-fn-handler-classes]] === Handler Classes @@ -380,7 +305,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 +313,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") } ---- @@ -401,88 +326,7 @@ Therefore, it is useful to group related handler functions together into a handl has a similar role as `@Controller` in an annotation-based application. For example, the following class exposes a reactive `Person` repository: --- -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.web.reactive.function.server.ServerResponse.ok; - -public class PersonHandler { - - private final PersonRepository repository; - - public PersonHandler(PersonRepository repository) { - this.repository = repository; - } - - public Mono listPeople(ServerRequest request) { // <1> - Flux people = repository.allPeople(); - return ok().contentType(APPLICATION_JSON).body(people, Person.class); - } - - public Mono createPerson(ServerRequest request) { // <2> - Mono person = request.bodyToMono(Person.class); - return ok().build(repository.savePerson(person)); - } - - public Mono getPerson(ServerRequest request) { // <3> - int personId = Integer.valueOf(request.pathVariable("id")); - return repository.getPerson(personId) - .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person)) - .switchIfEmpty(ServerResponse.notFound().build()); - } -} ----- -<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as -JSON. -<2> `createPerson` is a handler function that stores a new `Person` contained in the request body. -Note that `PersonRepository.savePerson(Person)` returns `Mono`: an empty `Mono` that emits -a completion signal when the person has been read from the request and stored. So we use the -`build(Publisher)` method to send a response when that completion signal is received (that is, -when the `Person` has been saved). -<3> `getPerson` is a handler function that returns a single person, identified by the `id` path -variable. We retrieve that `Person` from the repository and create a JSON response, if it is -found. If it is not found, we use `switchIfEmpty(Mono)` to return a 404 Not Found response. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class PersonHandler(private val repository: PersonRepository) { - - suspend fun listPeople(request: ServerRequest): ServerResponse { // <1> - val people: Flow = repository.allPeople() - return ok().contentType(APPLICATION_JSON).bodyAndAwait(people); - } - - suspend fun createPerson(request: ServerRequest): ServerResponse { // <2> - val person = request.awaitBody() - repository.savePerson(person) - return ok().buildAndAwait() - } - - suspend fun getPerson(request: ServerRequest): ServerResponse { // <3> - val personId = request.pathVariable("id").toInt() - return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) } - ?: ServerResponse.notFound().buildAndAwait() - - } - } ----- -<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as -JSON. -<2> `createPerson` is a handler function that stores a new `Person` contained in the request body. -Note that `PersonRepository.savePerson(Person)` is a suspending function with no return type. -<3> `getPerson` is a handler function that returns a single person, identified by the `id` path -variable. We retrieve that `Person` from the repository and create a JSON response, if it is -found. If it is not found, we return a 404 Not Found response. -====== --- - +include-code::./PersonHandler[tag=snippet,indent=0] [[webflux-fn-handler-validation]] === Validation @@ -491,73 +335,13 @@ A functional endpoint can use Spring's xref:web/webmvc/mvc-config/validation.ado apply validation to the request body. For example, given a custom Spring xref:web/webmvc/mvc-config/validation.adoc[Validator] implementation for a `Person`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class PersonHandler { - - private final Validator validator = new PersonValidator(); // <1> - - // ... - - public Mono createPerson(ServerRequest request) { - Mono person = request.bodyToMono(Person.class).doOnNext(this::validate); // <2> - return ok().build(repository.savePerson(person)); - } - - private void validate(Person person) { - Errors errors = new BeanPropertyBindingResult(person, "person"); - validator.validate(person, errors); - if (errors.hasErrors()) { - throw new ServerWebInputException(errors.toString()); // <3> - } - } - } ----- -<1> Create `Validator` instance. -<2> Apply validation. -<3> Raise exception for a 400 response. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class PersonHandler(private val repository: PersonRepository) { - - private val validator = PersonValidator() // <1> - - // ... - - suspend fun createPerson(request: ServerRequest): ServerResponse { - val person = request.awaitBody() - validate(person) // <2> - repository.savePerson(person) - return ok().buildAndAwait() - } - - private fun validate(person: Person) { - val errors: Errors = BeanPropertyBindingResult(person, "person"); - validator.validate(person, errors); - if (errors.hasErrors()) { - throw ServerWebInputException(errors.toString()) // <3> - } - } - } ----- -<1> Create `Validator` instance. -<2> Apply validation. -<3> Raise exception for a 400 response. -====== +include-code::./PersonHandler[tag=snippet,indent=0] Handlers can also use the standard bean validation API (JSR-303) by creating and injecting a global `Validator` instance based on `LocalValidatorFactoryBean`. See xref:core/validation/beanvalidation.adoc[Spring Validation]. - [[webflux-fn-router-functions]] == `RouterFunction` [.small]#xref:web/webmvc-functional.adoc#webmvc-fn-router-functions[See equivalent in the Servlet stack]# @@ -572,45 +356,24 @@ to create a router. Generally, it is recommended to use the `route()` builder, as it provides convenient short-cuts for typical mapping scenarios without requiring hard-to-discover static imports. -For instance, the router function builder offers the method `GET(String, HandlerFunction)` to create a mapping for GET requests; and `POST(String, HandlerFunction)` for POSTs. +For instance, the router function builder offers the method `GET(String, HandlerFunction)` +to create a mapping for GET requests; and `POST(String, HandlerFunction)` for POSTs. Besides HTTP method-based mapping, the route builder offers a way to introduce additional predicates when mapping to requests. For each HTTP method there is an overloaded variant that takes a `RequestPredicate` as a parameter, though which additional constraints can be expressed. - [[webflux-fn-predicates]] === Predicates You can write your own `RequestPredicate`, but the `RequestPredicates` utility class -offers commonly used implementations, based on the request path, HTTP method, content-type, -and so on. -The following example uses a request predicate to create a constraint based on the `Accept` -header: +offers built-in options for common needs for matching based on the HTTP method, request +path, headers, xref:#api-version[API version], and more. -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - RouterFunction route = RouterFunctions.route() - .GET("/hello-world", accept(MediaType.TEXT_PLAIN), - request -> ServerResponse.ok().bodyValue("Hello World")).build(); ----- +The following example uses an `Accept` header, request predicate: -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val route = coRouter { - GET("/hello-world", accept(TEXT_PLAIN)) { - ServerResponse.ok().bodyValueAndAwait("Hello World") - } - } ----- -====== +include-code::./RouterConfiguration[tag=snippet,indent=0] You can compose multiple request predicates together by using: @@ -623,8 +386,6 @@ and `RequestPredicates.path(String)`. The example shown above also uses two request predicates, as the builder uses `RequestPredicates.GET` internally, and composes that with the `accept` predicate. - - [[webflux-fn-routes]] === Routes @@ -647,62 +408,7 @@ There are also other ways to compose multiple router functions together: The following example shows the composition of four routes: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.web.reactive.function.server.RequestPredicates.*; - -PersonRepository repository = ... -PersonHandler handler = new PersonHandler(repository); - -RouterFunction otherRoute = ... - -RouterFunction route = route() - .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1> - .GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2> - .POST("/person", handler::createPerson) // <3> - .add(otherRoute) // <4> - .build(); ----- -<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to -`PersonHandler.getPerson` -<2> `GET /person` with an `Accept` header that matches JSON is routed to -`PersonHandler.listPeople` -<3> `POST /person` with no additional predicates is mapped to -`PersonHandler.createPerson`, and -<4> `otherRoute` is a router function that is created elsewhere, and added to the route built. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.http.MediaType.APPLICATION_JSON - - val repository: PersonRepository = ... - val handler = PersonHandler(repository); - - val otherRoute: RouterFunction = coRouter { } - - val route = coRouter { - GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1> - GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2> - POST("/person", handler::createPerson) // <3> - }.and(otherRoute) // <4> ----- -<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to -`PersonHandler.getPerson` -<2> `GET /person` with an `Accept` header that matches JSON is routed to -`PersonHandler.listPeople` -<3> `POST /person` with no additional predicates is mapped to -`PersonHandler.createPerson`, and -<4> `otherRoute` is a router function that is created elsewhere, and added to the route built. -====== - +include-code::./RouterConfiguration[tag=snippet,indent=0] [[nested-routes]] === Nested Routes @@ -719,7 +425,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 +438,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 +460,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 +473,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 +488,51 @@ Kotlin:: ====== + +[[api-version]] +=== API Version + +Router functions support matching by API version. + +First, enable API versioning in the +xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config], and then you can +use the `version` xref:#webflux-fn-predicates[predicate] as follows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + RouterFunction route = RouterFunctions.route() + .GET("/hello-world", version("1.2"), + request -> ServerResponse.ok().bodyValue("Hello World")).build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val route = coRouter { + GET("/hello-world", version("1.2")) { + ServerResponse.ok().bodyValueAndAwait("Hello World") + } + } +---- +====== + +The `version` predicate can be: + +- Fixed version ("1.2") -- matches the given version only +- Baseline version ("1.2+") -- matches the given version and above, up to the highest +xref:web/webmvc/mvc-config/api-version.adoc[supported version]. + +See xref:web/webflux-versioning.adoc[API Versioning] for more details on underlying +infrastructure and support for API Versioning. + + + + [[webflux-fn-serving-resources]] == Serving Resources @@ -800,11 +551,10 @@ for handling redirects in Single Page Applications. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- ClassPathResource index = new ClassPathResource("static/index.html"); - List extensions = List.of("js", "css", "ico", "png", "jpg", "gif"); - RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate(); + RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate(); RouterFunction redirectToIndex = route() .resource(spaPredicate, index) .build(); @@ -812,13 +562,11 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val redirectToIndex = router { val index = ClassPathResource("static/index.html") - val extensions = listOf("js", "css", "ico", "png", "jpg", "gif") - val spaPredicate = !(path("/api/**") or path("/error") or - pathExtension(extensions::contains)) + val spaPredicate = !(path("/api/**") or path("/error")) resource(spaPredicate, index) } ---- @@ -833,17 +581,17 @@ It is also possible to route requests that match a given pattern to resources re ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - Resource location = new FileSystemResource("public-resources/"); + Resource location = new FileUrlResource("public-resources/"); RouterFunction resources = RouterFunctions.resources("/resources/**", location); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val location = FileSystemResource("public-resources/") + val location = FileUrlResource("public-resources/") val resources = router { resources("/resources/**", location) } ---- ====== @@ -888,10 +636,9 @@ 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 public class WebConfig implements WebFluxConfigurer { @Bean @@ -925,10 +672,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class WebConfig : WebFluxConfigurer { @Bean @@ -959,8 +705,6 @@ Kotlin:: ====== - - [[webflux-fn-handler-filter-function]] == Filtering Handler Functions [.small]#xref:web/webmvc-functional.adoc#webmvc-fn-handler-filter-function[See equivalent in the Servlet stack]# @@ -976,7 +720,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 @@ -995,12 +739,12 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val route = router { - "/person".nest { + ("/person" and accept(APPLICATION_JSON)).nest { GET("/{id}", handler::getPerson) - GET("", handler::listPeople) + GET(handler::listPeople) before { // <1> ServerRequest.from(it) .header("X-RequestHeader", "Value").build() @@ -1027,54 +771,7 @@ Now we can add a simple security filter to our route, assuming that we have a `S can determine whether a particular path is allowed. The following example shows how to do so: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - SecurityManager securityManager = ... - - RouterFunction route = route() - .path("/person", b1 -> b1 - .nest(accept(APPLICATION_JSON), b2 -> b2 - .GET("/{id}", handler::getPerson) - .GET(handler::listPeople)) - .POST(handler::createPerson)) - .filter((request, next) -> { - if (securityManager.allowAccessTo(request.path())) { - return next.handle(request); - } - else { - return ServerResponse.status(UNAUTHORIZED).build(); - } - }) - .build(); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val securityManager: SecurityManager = ... - - val route = router { - ("/person" and accept(APPLICATION_JSON)).nest { - GET("/{id}", handler::getPerson) - GET("", handler::listPeople) - POST(handler::createPerson) - filter { request, next -> - if (securityManager.allowAccessTo(request.path())) { - next(request) - } - else { - status(UNAUTHORIZED).build(); - } - } - } - } ----- -====== +include-code::./RouterConfiguration[tag=snippet,indent=0] The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional. We only let the handler function be run when access is allowed. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-http-interface-client.adoc b/framework-docs/modules/ROOT/pages/web/webflux-http-interface-client.adoc deleted file mode 100644 index 871667acaaa9..000000000000 --- a/framework-docs/modules/ROOT/pages/web/webflux-http-interface-client.adoc +++ /dev/null @@ -1,10 +0,0 @@ -[[webflux-http-interface-client]] -= HTTP Interface Client - -The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP -exchange methods. You can then generate a proxy that implements this interface and -performs the exchanges. This helps to simplify HTTP remote access and provides additional -flexibility for to choose an API style such as synchronous or reactive. - -See xref:integration/rest-clients.adoc#rest-http-interface[REST Endpoints] for details. - diff --git a/framework-docs/modules/ROOT/pages/web/webflux-http-service-client.adoc b/framework-docs/modules/ROOT/pages/web/webflux-http-service-client.adoc new file mode 100644 index 000000000000..f083a87545ab --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webflux-http-service-client.adoc @@ -0,0 +1,9 @@ +[[webflux-http-service-client]] += HTTP Service Client + +The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP +exchange methods. You can then generate a proxy that implements this interface and +performs the exchanges. This helps to simplify HTTP remote access and provides additional +flexibility in choosing an API style such as synchronous or reactive. + +See xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service Clients] for details. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-test.adoc b/framework-docs/modules/ROOT/pages/web/webflux-test.adoc index ed93058dac2b..39bd3906e872 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-test.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-test.adoc @@ -9,4 +9,4 @@ discussion of mock objects. xref:testing/webtestclient.adoc[`WebTestClient`] builds on these mock request and response objects to provide support for testing WebFlux applications without an HTTP -server. You can use the `WebTestClient` for end-to-end integration tests, too. \ No newline at end of file +server. You can use the `WebTestClient` for end-to-end integration tests, too. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc b/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc new file mode 100644 index 000000000000..50584235956f --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc @@ -0,0 +1,108 @@ +[[webflux-versioning]] += API Versioning +:page-section-summary-toc: 1 + +[.small]#xref:web/webmvc-versioning.adoc[See equivalent in the Servlet stack]# + +Spring WebFlux supports API versioning. This section provides an overview of the support +and underlying strategies. + +Please, see also related content in: + +- Configure xref:web/webflux/config.adoc#webflux-config-api-version[API versioning] +in the WebFlux Config +- xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[Map requests] +to annotated controller methods with an API version +- xref:web/webflux-functional.adoc#api-version[Route requests] +to functional endpoints with an API version + +Client support for API versioning is available also in `RestClient`, `WebClient`, and +xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service] clients, as well as +for testing in `WebTestClient`. + + + + +[[webflux-versioning-strategy]] +== ApiVersionStrategy +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[See equivalent in the Servlet stack]# + +This is the central strategy for API versioning that holds all configured preferences +related to versioning. It does the following: + +- Resolves versions from the requests via xref:#webflux-versioning-resolver[ApiVersionResolver] +- Parses raw version values into `Comparable` with xref:#webflux-versioning-parser[ApiVersionParser] +- xref:#webflux-versioning-validation[Validates] request versions + +`ApiVersionStrategy` helps to map requests to `@RequestMapping` controller methods, +and is initialized by the WebFlux config. Typically, applications do not interact +directly with it. + + + + +[[webflux-versioning-resolver]] +== ApiVersionResolver +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-resolver[See equivalent in the Servlet stack]# + +This strategy resolves the API version from a request. The WebFlux config provides built-in +options to resolve from a header, query parameter, media type parameter, +or from the URL path. You can also use a custom `ApiVersionResolver`. + +The path resolver selects the version from a path segment specified by index, or +raises `InvalidApiVersionException`, and therefore never results in `null` (no version) +unless it is configured with a `Predicate` to determine if a path is versioned. + + + + +[[webflux-versioning-parser]] +== ApiVersionParser +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-parser[See equivalent in the Servlet stack]# + +This strategy helps to parse raw version values into `Comparable`, which helps to +compare, sort, and select versions. By default, the built-in `SemanticApiVersionParser` +parses a version into `major`, `minor`, and `patWebFluxch` integer values. Minor and patch +values are set to 0 if not present. + + + + +[[webflux-versioning-validation]] +== Validation +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-validation[See equivalent in the Servlet stack]# + +If a request version is not supported, `InvalidApiVersionException` is raised resulting +in a 400 response. By default, the list of supported versions is initialized from declared +versions in annotated controller mappings, but you can turn that off through a flag in the +WebFlux config, and use only the versions configured explicitly in the config. + +By default, a version is required when API versioning is enabled, and +`MissingApiVersionException` is raised resulting in a 400 response if not present. +You can make it optional in which case the most recent version is used. +You can also specify a default version to use. + + + + +[[webflux-versioning-deprecation-handler]] +== ApiVersionDeprecationHandler +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-deprecation-handler[See equivalent in the Reactive stack]# + +This strategy can be configured to send hints and information about deprecated versions to +clients via response headers. The built-in `StandardApiVersionDeprecationHandler` +can set the "Deprecation" "Sunset" headers and "Link" headers as defined in +https://datatracker.ietf.org/doc/html/rfc9745[RFC 9745] and +https://datatracker.ietf.org/doc/html/rfc8594[RFC 8594]. You can also configure a custom +handler for different headers. + + + + +[[webflux-versioning-mapping]] +== Request Mapping +[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-mapping[See equivalent in the Servlet stack]# + +`ApiVersionStrategy` supports the mapping of requests to annotated controller methods. +See xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[API Versions] +for more details. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webflux-view.adoc b/framework-docs/modules/ROOT/pages/web/webflux-view.adoc index 288fc1a38c52..8d34f5072edf 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-view.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-view.adoc @@ -1,13 +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. [[webflux-view-thymeleaf]] @@ -29,8 +35,6 @@ https://www.thymeleaf.org/documentation.html[Thymeleaf+Spring] and the WebFlux i https://web.archive.org/web/20210623051330/http%3A//forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html[announcement]. - - [[webflux-view-freemarker]] == FreeMarker [.small]#xref:web/webmvc-view/mvc-freemarker.adoc[See equivalent in the Servlet stack]# @@ -39,8 +43,6 @@ https://freemarker.apache.org/[Apache FreeMarker] is a template engine for gener kind of text output from HTML to email and others. The Spring Framework has built-in integration for using Spring WebFlux with FreeMarker templates. - - [[webflux-view-freemarker-contextconfig]] === View Configuration [.small]#xref:web/webmvc-view/mvc-freemarker.adoc#mvc-view-freemarker-contextconfig[See equivalent in the Servlet stack]# @@ -51,11 +53,10 @@ 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 - public class WebConfig implements WebFluxConfigurer { + public class WebConfiguration implements WebFluxConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { @@ -75,11 +76,10 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux - class WebConfig : WebFluxConfigurer { + class WebConfiguration : WebFluxConfigurer { override fun configureViewResolvers(registry: ViewResolverRegistry) { registry.freeMarker() @@ -100,8 +100,6 @@ shown in the preceding example. Given the preceding configuration, if your contr returns the view name, `welcome`, the resolver looks for the `classpath:/templates/freemarker/welcome.ftl` template. - - [[webflux-views-freemarker]] === FreeMarker Configuration [.small]#xref:web/webmvc-view/mvc-freemarker.adoc#mvc-views-freemarker[See equivalent in the Servlet stack]# @@ -116,11 +114,10 @@ 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 - public class WebConfig implements WebFluxConfigurer { + public class WebConfiguration implements WebFluxConfigurer { // ... @@ -139,11 +136,10 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux - class WebConfig : WebFluxConfigurer { + class WebConfiguration : WebFluxConfigurer { // ... @@ -159,8 +155,6 @@ Kotlin:: See the FreeMarker documentation for details of settings and variables as they apply to the `Configuration` object. - - [[webflux-view-freemarker-forms]] === Form Handling [.small]#xref:web/webmvc-view/mvc-freemarker.adoc#mvc-view-freemarker-forms[See equivalent in the Servlet stack]# @@ -171,7 +165,6 @@ form-backing objects and show the results of failed validations from a `Validato web or business tier. Spring also has support for the same functionality in FreeMarker, with additional convenience macros for generating form input elements themselves. - [[webflux-view-bind-macros]] ==== The Bind Macros [.small]#xref:web/webmvc-view/mvc-freemarker.adoc#mvc-view-bind-macros[See equivalent in the Servlet stack]# @@ -189,7 +182,6 @@ directly, the file is called `spring.ftl` and is in the For additional details on binding support, see xref:web/webmvc-view/mvc-freemarker.adoc#mvc-view-simple-binding[Simple Binding] for Spring MVC. - [[webflux-views-form-macros]] ==== Form Macros @@ -202,7 +194,6 @@ sections of the Spring MVC documentation. * xref:web/webmvc-view/mvc-freemarker.adoc#mvc-views-form-macros-html-escaping[HTML Escaping] - [[webflux-view-script]] == Script Views [.small]#xref:web/webmvc-view/mvc-script.adoc[See equivalent in the Servlet stack]# @@ -215,39 +206,21 @@ The following table shows the templating libraries that we have tested on differ [%header] |=== |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://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby] |https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython] -|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin] |=== TIP: The basic rule for integrating any other script engine is that it must implement the `ScriptEngine` and `Invocable` interfaces. - - [[webflux-view-script-dependencies]] === Requirements [.small]#xref:web/webmvc-view/mvc-script.adoc#mvc-view-script-dependencies[See equivalent in the Servlet stack]# You need to have the script engine on your classpath, the details of which vary by script engine: -* The https://openjdk.java.net/projects/nashorn/[Nashorn] JavaScript engine is provided with -Java 8+. Using the latest update release available is highly recommended. * https://www.jruby.org[JRuby] should be added as a dependency for Ruby support. * https://www.jython.org[Jython] should be added as a dependency for Python support. -* `org.jetbrains.kotlin:kotlin-script-util` dependency and a `META-INF/services/javax.script.ScriptEngineFactory` - file containing a `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory` - line should be added for Kotlin script support. See - https://github.com/sdeleuze/kotlin-script-templating[this example] for more detail. - -You need to have the script templating library. One way to do that for JavaScript is -through https://www.webjars.org/[WebJars]. - - [[webflux-view-script-integrate]] === Script Templates @@ -255,17 +228,16 @@ through https://www.webjars.org/[WebJars]. 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. -The following example uses Mustache templates and the Nashorn JavaScript engine: +The following example uses the Jython Python engine: [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 { + public class WebConfiguration implements WebFluxConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { @@ -275,9 +247,8 @@ Java:: @Bean public ScriptTemplateConfigurer configurer() { ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); - configurer.setEngineName("nashorn"); - configurer.setScripts("mustache.js"); - configurer.setRenderObject("Mustache"); + configurer.setEngineName("jython"); + configurer.setScripts("render.py"); configurer.setRenderFunction("render"); return configurer; } @@ -286,11 +257,10 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux - class WebConfig : WebFluxConfigurer { + class WebConfiguration : WebFluxConfigurer { override fun configureViewResolvers(registry: ViewResolverRegistry) { registry.scriptTemplate() @@ -298,9 +268,8 @@ Kotlin:: @Bean fun configurer() = ScriptTemplateConfigurer().apply { - engineName = "nashorn" - setScripts("mustache.js") - renderObject = "Mustache" + engineName = "jython" + setScripts("render.py") renderFunction = "render" } } @@ -314,117 +283,101 @@ The `render` function is called with the following parameters: * `RenderingContext renderingContext`: The {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) + URL + +Check out the Spring Framework unit tests, +{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]# -`Mustache.render()` is natively compatible with this signature, so you can call it directly. +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. -If your templating technology requires some customization, you can provide a script that -implements a custom render function. For example, https://handlebarsjs.com[Handlerbars] -needs to compile templates before using them and requires a -https://en.wikipedia.org/wiki/Polyfill[polyfill] in order to emulate some -browser facilities not available in the server-side script engine. -The following example shows how to set a custom render function: +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",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - @Configuration - @EnableWebFlux - public class WebConfig implements WebFluxConfigurer { - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.scriptTemplate(); - } - - @Bean - public ScriptTemplateConfigurer configurer() { - ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); - configurer.setEngineName("nashorn"); - configurer.setScripts("polyfill.js", "handlebars.js", "render.js"); - configurer.setRenderFunction("render"); - configurer.setSharedEngine(false); - return configurer; - } + @GetMapping + List handle() { + return List.of(Fragment.create("posts"), Fragment.create("comments")); } ---- 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) { - registry.scriptTemplate() - } - - @Bean - fun configurer() = ScriptTemplateConfigurer().apply { - engineName = "nashorn" - setScripts("polyfill.js", "handlebars.js", "render.js") - renderFunction = "render" - isSharedEngine = false - } + @GetMapping + fun handle(): List { + return listOf(Fragment.create("posts"), Fragment.create("comments")) } ---- ====== -NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe -script engines with templating libraries not designed for concurrency, such as Handlebars or -React running on Nashorn. In that case, Java SE 8 update 60 is required, due to -https://bugs.openjdk.java.net/browse/JDK-8076099[this bug], but it is generally -recommended to use a recent Java SE patch release in any case. +The same can be done also by returning the dedicated type `FragmentsRendering`: -`polyfill.js` defines only the `window` object needed by Handlebars to run properly, -as the following snippet shows: - -[source,javascript,indent=0,subs="verbatim,quotes"] +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] ---- - var window = {}; + @GetMapping + FragmentsRendering handle() { + return FragmentsRendering.fragment("posts").fragment("comments").build(); + } ---- -This basic `render.js` implementation compiles the template before using it. A production -ready implementation should also store and reused cached templates or pre-compiled templates. -This can be done on the script side, as well as any customization you need (managing -template engine configuration for example). -The following example shows how compile a template: - -[source,javascript,indent=0,subs="verbatim,quotes"] +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - function render(template, model) { - var compiledTemplate = Handlebars.compile(template); - return compiledTemplate(model); + @GetMapping + fun handle(): FragmentsRendering { + return FragmentsRendering.fragment("posts").fragment("comments").build() } ---- +====== -Check out the Spring Framework unit tests, -{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. - +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]# -For xref:web/webflux/dispatcher-handler.adoc#webflux-multiple-representations[Content Negotiation] purposes, it is useful to be able to alternate -between rendering a model with an HTML template or as other formats (such as JSON or XML), -depending on the content type requested by the client. To support doing so, Spring WebFlux -provides the `HttpMessageWriterView`, which you can use to plug in any of the available -xref:web/webflux/reactive-spring.adoc#webflux-codecs[Codecs] from `spring-web`, such as `Jackson2JsonEncoder`, `Jackson2SmileEncoder`, -or `Jaxb2XmlEncoder`. +For xref:web/webflux/dispatcher-handler.adoc#webflux-multiple-representations[Content Negotiation] +purposes, it is useful to be able to alternate between rendering a model with an HTML template +or as other formats (such as JSON or XML), depending on the content type requested by the client. +To support doing so, Spring WebFlux provides the `HttpMessageWriterView`, which you can use to +plug in any of the available xref:web/webflux/reactive-spring.adoc#webflux-codecs[Codecs] from +`spring-web`, such as `JacksonJsonEncoder`, `JacksonSmileEncoder`, or `Jaxb2XmlEncoder`. -Unlike other view technologies, `HttpMessageWriterView` does not require a `ViewResolver` -but is instead xref:web/webflux/config.adoc#webflux-config-view-resolvers[configured] as a default view. You can -configure one or more such default views, wrapping different `HttpMessageWriter` instances +Unlike other view technologies, `HttpMessageWriterView` does not require a `ViewResolver` but is +instead xref:web/webflux/config.adoc#webflux-config-view-resolvers[configured] as a default view. +You can configure one or more such default views, wrapping different `HttpMessageWriter` instances or `Encoder` instances. The one that matches the requested content type is used at runtime. In most cases, a model contains multiple attributes. To determine which one to serialize, diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc index 448ff3db92d8..a8b3b595ed3e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc @@ -2,22 +2,18 @@ = WebClient :page-section-summary-toc: 1 -Spring WebFlux includes a client to perform HTTP requests with. `WebClient` has a -functional, fluent API based on Reactor, see xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries], +Spring WebFlux includes a client to perform HTTP requests. `WebClient` has a +functional, fluent API based on Reactor (see xref:web/webflux-reactive-libraries.adoc[Reactive Libraries]) which enables declarative composition of asynchronous logic without the need to deal with -threads or concurrency. It is fully non-blocking, it supports streaming, and relies on +threads or concurrency. It is fully non-blocking, supports streaming, and relies on the same xref:web/webflux/reactive-spring.adoc#webflux-codecs[codecs] that are also used to encode and decode request and response content on the server side. -`WebClient` needs an HTTP client library to perform requests with. There is built-in +`WebClient` needs an HTTP client library to perform requests. There is built-in support for the following: * {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`. - - - - +* Others can be plugged in 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..f91ed00d9c62 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, _ -> @@ -48,5 +48,3 @@ Note that you can configure a `defaultRequest` callback globally at the `WebClient.Builder` level which lets you insert attributes into all requests, which could be used for example in a Spring MVC application to populate request attributes based on `ThreadLocal` data. - - 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..068b40869db5 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 = ... @@ -103,7 +103,6 @@ Kotlin:: ====== - [[webflux-client-body-form]] == Form Data @@ -115,7 +114,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 +127,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val formData: MultiValueMap = ... @@ -146,7 +145,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 +158,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.* @@ -172,7 +171,6 @@ Kotlin:: ====== - [[webflux-client-body-multipart]] == Multipart Data @@ -185,7 +183,7 @@ multipart request. The following example shows how to create a `MultiValueMap result = webClient - .post() - .uri("https://example.com") - .body(Flux.concat( - FormPartEvent.create("field", "field value"), - FilePartEvent.create("file", resource) - ), PartEvent.class) - .retrieve() - .bodyToMono(String.class); + Resource resource = ... + Mono result = webClient + .post() + .uri("https://example.com") + .body(Flux.concat( + FormPartEvent.create("field", "field value"), + FilePartEvent.create("file", resource) + ), PartEvent.class) + .retrieve() + .bodyToMono(String.class); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- -var resource: Resource = ... -var result: Mono = webClient - .post() - .uri("https://example.com") - .body( - Flux.concat( - FormPartEvent.create("field", "field value"), - FilePartEvent.create("file", resource) +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + var resource: Resource = ... + var result: Mono = webClient + .post() + .uri("https://example.com") + .body( + Flux.concat( + FormPartEvent.create("field", "field value"), + FilePartEvent.create("file", resource) + ) ) - ) - .retrieve() - .bodyToMono() + .retrieve() + .bodyToMono() ---- ====== On the server side, `PartEvent` objects that are received via `@RequestBody` or `ServerRequest::bodyToFlux(PartEvent.class)` can be relayed to another service via the `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..c546f64c4e8b 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc @@ -1,7 +1,7 @@ [[webflux-client-builder]] = Configuration -The simplest way to create a `WebClient` is through one of the static factory methods: +The simplest way to create `WebClient` is through one of the static factory methods: * `WebClient.create()` * `WebClient.create(String baseUrl)` @@ -12,12 +12,14 @@ You can also use `WebClient.builder()` with further options: * `defaultUriVariables`: default values to use when expanding URI templates. * `defaultHeader`: Headers for every request. * `defaultCookie`: Cookies for every request. +* `defaultApiVersion`: API version for every request. * `defaultRequest`: `Consumer` to customize every request. * `filter`: Client filter for every request. * `exchangeStrategies`: HTTP message reader/writer customizations. * `clientConnector`: HTTP client library settings. -* `observationRegistry`: the registry to use for enabling xref:integration/observability.adoc#http-client.webclient[Observability support]. -* `observationConvention`: xref:integration/observability.adoc#config[an optional, custom convention to extract metadata] for recorded observations. +* `apiVersionInserter`: to insert API version values in the request +* `observationRegistry`: the registry to use for enabling xref:integration/observability.adoc#observability.http-client.webclient[Observability support]. +* `observationConvention`: xref:integration/observability.adoc#observability.config[an optional, custom convention to extract metadata] for recorded observations. For example: @@ -25,7 +27,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 +36,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 +51,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 +66,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() @@ -78,6 +80,7 @@ Kotlin:: ---- ====== + [[webflux-client-builder-maxinmemorysize]] == MaxInMemorySize @@ -95,7 +98,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 +107,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) } @@ -113,7 +116,6 @@ Kotlin:: ====== - [[webflux-client-builder-reactor]] == Reactor Netty @@ -123,7 +125,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 +136,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val httpClient = HttpClient.create().secure { ... } @@ -144,7 +146,6 @@ Kotlin:: ---- ====== - [[webflux-client-builder-reactor-resources]] === Resources @@ -165,7 +166,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 +176,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 +193,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 +221,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean fun resourceFactory() = ReactorResourceFactory().apply { @@ -245,7 +246,6 @@ Kotlin:: ====== -- - [[webflux-client-builder-reactor-timeout]] === Timeouts @@ -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") @@ -378,7 +378,6 @@ Kotlin:: ====== - [[webflux-client-builder-jdk-httpclient]] == JDK HttpClient @@ -388,36 +387,35 @@ 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) - .connectTimeout(Duration.ofSeconds(20)) - .build(); + HttpClient httpClient = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(20)) + .build(); - ClientHttpConnector connector = - new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory()); + ClientHttpConnector connector = + new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory()); - WebClient webClient = WebClient.builder().clientConnector(connector).build(); + WebClient webClient = WebClient.builder().clientConnector(connector).build(); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val httpClient = HttpClient.newBuilder() - .followRedirects(Redirect.NORMAL) - .connectTimeout(Duration.ofSeconds(20)) - .build() + val httpClient = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(20)) + .build() - val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory()) + val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory()) - val webClient = WebClient.builder().clientConnector(connector).build() + val webClient = WebClient.builder().clientConnector(connector).build() ---- ====== - [[webflux-client-builder-jetty]] == Jetty @@ -428,7 +426,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 +438,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 +463,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 +487,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Bean fun resourceFactory() = JettyResourceFactory() @@ -511,7 +509,6 @@ Kotlin:: -- - [[webflux-client-builder-http-components]] == HttpComponents @@ -521,7 +518,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 +531,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(...) @@ -543,5 +540,3 @@ Kotlin:: val webClient = WebClient.builder().clientConnector(connector).build() ---- ====== - - 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..c2b64314b2b1 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) -> @@ -32,6 +32,3 @@ Java:: .contextWrite(context -> context.put("foo", ...)); ---- ====== - - - 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..0fe359c64b40 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") @@ -47,7 +47,3 @@ When using the above, after the returned `Mono` or `Flux` completes, the respons is checked and if not consumed it is released to prevent memory and connection leaks. Therefore the response cannot be decoded further downstream. It is up to the provided function to declare how to decode the response if needed. - - - - 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..a2d4ad961f86 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc @@ -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,67 +156,67 @@ a custom filter class that helps with computing a `Content-Length` header for `P ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- -public class MultipartExchangeFilterFunction implements ExchangeFilterFunction { - - @Override - public Mono filter(ClientRequest request, ExchangeFunction next) { - if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) - && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { - return next.exchange(ClientRequest.from(request).body((outputMessage, context) -> - request.body().insert(new BufferingDecorator(outputMessage), context)).build() - ); - } else { - return next.exchange(request); - } - } - - private static final class BufferingDecorator extends ClientHttpRequestDecorator { - - private BufferingDecorator(ClientHttpRequest delegate) { - super(delegate); - } - - @Override - public Mono writeWith(Publisher body) { - return DataBufferUtils.join(body).flatMap(buffer -> { - getHeaders().setContentLength(buffer.readableByteCount()); - return super.writeWith(Mono.just(buffer)); - }); - } - } -} +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class MultipartExchangeFilterFunction implements ExchangeFilterFunction { + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) + && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { + return next.exchange(ClientRequest.from(request).body((outputMessage, context) -> + request.body().insert(new BufferingDecorator(outputMessage), context)).build() + ); + } else { + return next.exchange(request); + } + } + + private static final class BufferingDecorator extends ClientHttpRequestDecorator { + + private BufferingDecorator(ClientHttpRequest delegate) { + super(delegate); + } + + @Override + public Mono writeWith(Publisher body) { + return DataBufferUtils.join(body).flatMap(buffer -> { + getHeaders().setContentLength(buffer.readableByteCount()); + return super.writeWith(Mono.just(buffer)); + }); + } + } + } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- -class MultipartExchangeFilterFunction : ExchangeFilterFunction { - - override fun filter(request: ClientRequest, next: ExchangeFunction): Mono { - return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) - && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { - next.exchange(ClientRequest.from(request) - .body { message, context -> request.body().insert(BufferingDecorator(message), context) } - .build()) - } - else { - next.exchange(request) - } - - } - - private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) { - override fun writeWith(body: Publisher): Mono { - return DataBufferUtils.join(body) - .flatMap { - headers.contentLength = it.readableByteCount().toLong() - super.writeWith(Mono.just(it)) - } - } - } -} ----- -====== \ No newline at end of file +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + class MultipartExchangeFilterFunction : ExchangeFilterFunction { + + override fun filter(request: ClientRequest, next: ExchangeFunction): Mono { + return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) + && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { + next.exchange(ClientRequest.from(request) + .body { message, context -> request.body().insert(BufferingDecorator(message), context) } + .build()) + } + else { + next.exchange(request) + } + + } + + private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) { + override fun writeWith(body: Publisher): Mono { + return DataBufferUtils.join(body) + .flatMap { + headers.contentLength = it.readableByteCount().toLong() + super.writeWith(Mono.just(it)) + } + } + } + } +---- +====== 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..da9d991e47c4 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,29 +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..b0174ed26c44 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 { @@ -91,7 +91,3 @@ Simply return the resulting reactive type from the controller method. The same p Kotlin Coroutines and Spring WebFlux, just use suspending function or return `Flow` in your controller method . ==== - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc index febbb5498272..f7a69c7dbb3d 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-testing.adoc @@ -2,9 +2,16 @@ = Testing :page-section-summary-toc: 1 -To test code that uses the `WebClient`, you can use a mock web server, such as the -https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer]. To see an example -of its use, check out +To test code that uses the `WebClient`, you can use a mock web server, such as +https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or +https://wiremock.org/[WireMock]. Mock web servers accept requests over HTTP like a regular +server, and that means you can test with the same HTTP client that is also configured in +the same way as in production, which is important because there are often subtle +differences in the way different clients handle network I/O. Another advantage of mock +web servers is the ability to simulate specific network issues and conditions at the +transport level, in combination with the client used in production. + +For example use of MockWebServer, see {spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`] in the Spring Framework test suite or the https://github.com/square/okhttp/tree/master/samples/static-server[`static-server`] diff --git a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc index 204c5f771fe8..e08eed918259 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 @@ -7,6 +8,7 @@ messaging. include::partial$web/websocket-intro.adoc[leveloffset=+1] + [[webflux-websocket-server]] == WebSocket API [.small]#xref:web/websocket/stomp/server-config.adoc[See equivalent in the Servlet stack]# @@ -14,8 +16,6 @@ include::partial$web/websocket-intro.adoc[leveloffset=+1] The Spring Framework provides a WebSocket API that you can use to write client- and server-side applications that handle WebSocket messages. - - [[webflux-websocket-server-handler]] === Server [.small]#xref:web/websocket/server.adoc#websocket-server-handler[See equivalent in the Servlet stack]# @@ -27,7 +27,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 +43,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 +63,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 +81,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig { @@ -105,7 +105,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 +121,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig { @@ -134,8 +134,6 @@ Kotlin:: ---- ====== - - [[webflux-websockethandler]] === `WebSocketHandler` @@ -177,7 +175,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,19 +199,19 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleHandler : WebSocketHandler { override fun handle(session: WebSocketSession): Mono { - return session.receive() // <1> + return session.receive() // <1> .doOnNext { // ... // <2> } .concatMap { // ... // <3> } - .then() // <4> + .then() // <4> } } ---- @@ -235,7 +233,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,22 +259,22 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleHandler : WebSocketHandler { override fun handle(session: WebSocketSession): Mono { - val output = session.receive() // <1> + val output = session.receive() // <1> .doOnNext { // ... } .concatMap { // ... } - .map { session.textMessage("Echo $it") } // <2> + .map { session.textMessage("Echo $it") } // <2> - return session.send(output) // <3> + return session.send(output) // <3> } } ---- @@ -293,7 +291,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 { @@ -312,7 +310,7 @@ Java:: Flux source = ... ; Mono output = session.send(source.map(session::textMessage)); <2> - return Mono.zip(input, output).then(); <3> + return input.and(output); <3> } } ---- @@ -322,7 +320,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class ExampleHandler : WebSocketHandler { @@ -340,7 +338,7 @@ Kotlin:: val source: Flux = ... val output = session.send(source.map(session::textMessage)) // <2> - return Mono.zip(input, output).then() // <3> + return input.and(output) // <3> } } ---- @@ -349,8 +347,6 @@ Kotlin:: <3> Join the streams and return a `Mono` that completes when either stream ends. ====== - - [[webflux-websocket-databuffer]] === `DataBuffer` @@ -364,9 +360,6 @@ When running on Netty, applications must use `DataBufferUtils.retain(dataBuffer) wish to hold on input data buffers in order to ensure they are not released, and subsequently use `DataBufferUtils.release(dataBuffer)` when the buffers are consumed. - - - [[webflux-websocket-server-handshake]] === Handshake [.small]#xref:web/websocket/server.adoc#websocket-server-handshake[See equivalent in the Servlet stack]# @@ -374,14 +367,12 @@ subsequently use `DataBufferUtils.release(dataBuffer)` when the buffers are cons `WebSocketHandlerAdapter` delegates to a `WebSocketService`. By default, that is an instance of `HandshakeWebSocketService`, which performs basic checks on the WebSocket request and then uses `RequestUpgradeStrategy` for the server in use. Currently, there is built-in -support for Reactor Netty, Tomcat, Jetty, and Undertow. +support for Reactor Netty, Tomcat, and Jetty. `HandshakeWebSocketService` exposes a `sessionAttributePredicate` property that allows setting a `Predicate` to extract attributes from the `WebSession` and insert them into the attributes of the `WebSocketSession`. - - [[webflux-websocket-server-config]] === Server Configuration [.small]#xref:web/websocket/server.adoc#websocket-server-runtime-configuration[See equivalent in the Servlet stack]# @@ -396,7 +387,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 +408,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig { @@ -440,8 +431,6 @@ Kotlin:: Check the upgrade strategy for your server to see what options are available. Currently, only Tomcat and Jetty expose such options. - - [[webflux-websocket-server-cors]] === CORS [.small]#xref:web/websocket/server.adoc#websocket-server-allowed-origins[See equivalent in the Servlet stack]# @@ -453,13 +442,11 @@ that, you can also set the `corsConfigurations` property on the `SimpleUrlHandle specify CORS settings by URL pattern. If both are specified, they are combined by using the `combine` method on `CorsConfiguration`. - - [[webflux-websocket-client]] === Client Spring WebFlux provides a `WebSocketClient` abstraction with implementations for -Reactor Netty, Tomcat, Jetty, Undertow, and standard Java (that is, JSR-356). +Reactor Netty, Tomcat, Jetty, and standard Java (that is, JSR-356). NOTE: The Tomcat client is effectively an extension of the standard Java one with some extra functionality in the `WebSocketSession` handling to take advantage of the Tomcat-specific @@ -472,7 +459,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 +472,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 cbf487481cb8..ffc5729b79b7 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux.adoc @@ -8,14 +8,10 @@ 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 {reactive-streams-site}/[Reactive Streams] back pressure, and runs on such servers as -Netty, Undertow, and Servlet containers. +Netty, and Servlet containers. Both web frameworks mirror the names of their source modules ({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 e75be6630c42..023df320b153 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 @@ -22,7 +22,6 @@ xref:web/webflux/controller/ann-advice.adoc[@ControllerAdvice] that handles all and any `ErrorResponseException`, and renders an error response with a body. - [[webflux-ann-rest-exceptions-render]] == Render [.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-render[See equivalent in the Servlet stack]# @@ -33,9 +32,9 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a - The `status` property of `ProblemDetail` determines the HTTP status. - The `instance` property of `ProblemDetail` is set from the current URL path, if not already set. -- For content negotiation, the Jackson `HttpMessageConverter` prefers -"application/problem+json" over "application/json" when rendering a `ProblemDetail`, -and also falls back on it if no compatible media type is found. +- The Jackson JSON and XML message converters use "application/problem+json" or +"application/problem+xml" respectively as the producible media types for `ProblemDetail` +to ensure they are favored for content negotiation. To enable RFC 9457 responses for Spring WebFlux exceptions and for any `ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an @@ -46,8 +45,7 @@ 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 7807 response and take some action. - +any RFC 9457 response and take some action. [[webflux-ann-rest-exceptions-non-standard]] @@ -64,10 +62,16 @@ 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. +TIP: In Spring Boot, the `spring.webflux.problemdetails.enabled` property autoconfigures +a `ResponseEntityExceptionHandler` that handles built-in exceptions with problem details. +In that case, you may prefer to create another `@ControllerAdvice` instead of extending +`ResponseEntityExceptionHandler` if you want to take over the handling of a specific +built-in exception. You'll need to ensure your handler is ordered ahead of the one +configured by Spring Boot whose order is 0. [[webflux-ann-rest-exceptions-i18n]] @@ -107,7 +111,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) @@ -138,6 +142,10 @@ Message codes and arguments for each error are also resolved via `MessageSource` | `+{0}+` the list of global errors, `+{1}+` the list of field errors. Message codes and arguments for each error are also resolved via `MessageSource`. +| `NoResourceFoundException` +| (default) +| `+{0}+` the request path (or portion of) used to find a resource + |=== NOTE: Unlike other exceptions, the message arguments for @@ -149,8 +157,6 @@ xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method for more details. - - [[webflux-ann-rest-exceptions-client]] == Client Handling [.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-client[See equivalent in the Servlet stack]# @@ -159,7 +165,3 @@ A client application can catch `WebClientResponseException`, when using the `Web or `RestClientResponseException` when using the `RestTemplate`, and use their `getResponseBodyAs` methods to decode the error response body to any target type such as `ProblemDetail`, or a subclass of `ProblemDetail`. - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc b/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc index a01e743b5edf..7c39b82f5cd1 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/caching.adoc @@ -14,7 +14,6 @@ the `Last-Modified` header. This section describes the HTTP caching related options available in Spring WebFlux. - [[webflux-caching-cachecontrol]] == `CacheControl` [.small]#xref:web/webmvc/mvc-caching.adoc#mvc-caching-cachecontrol[See equivalent in the Servlet stack]# @@ -34,7 +33,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 +49,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) @@ -67,7 +66,6 @@ Kotlin:: ====== - [[webflux-caching-etag-lastmodified]] == Controllers [.small]#xref:web/webmvc/mvc-caching.adoc#mvc-caching-etag-lastmodified[See equivalent in the Servlet stack]# @@ -82,7 +80,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 +98,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 +128,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 +149,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? { @@ -178,11 +176,10 @@ values, or both. For conditional `GET` and `HEAD` requests, you can set the resp to 412 (PRECONDITION_FAILED) to prevent concurrent modification. - [[webflux-caching-static-resources]] == Static Resources [.small]#xref:web/webmvc/mvc-caching.adoc#mvc-caching-static-resources[See equivalent in the Servlet stack]# You should serve static resources with a `Cache-Control` and conditional response headers -for optimal performance. See the section on configuring xref:web/webflux/config.adoc#webflux-config-static-resources[Static Resources]. - +for optimal performance. See the section on configuring +xref:web/webflux/config.adoc#webflux-config-static-resources[Static Resources]. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc index 39092f7b3b73..9c255395a730 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc @@ -15,7 +15,6 @@ gain full control over the configuration through the xref:web/webflux/config.adoc#webflux-config-advanced-java[Advanced Configuration Mode]. - [[webflux-config-enable]] == Enabling WebFlux Config [.small]#xref:web/webmvc/mvc-config/enable.adoc[See equivalent in the Servlet stack]# @@ -26,7 +25,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 +35,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @EnableWebFlux @@ -47,14 +46,13 @@ 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}/web.html#web.reactive.webflux.auto-configuration[the dedicated Spring Boot documentation]. +{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. - [[webflux-config-customize]] == WebFlux config API [.small]#xref:web/webmvc/mvc-config/customize.adoc[See equivalent in the Servlet stack]# @@ -66,7 +64,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 implements WebFluxConfigurer { @@ -77,7 +75,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -88,13 +86,13 @@ class WebConfig : WebFluxConfigurer { ====== - [[webflux-config-conversion]] == Conversion, formatting [.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: @@ -102,7 +100,7 @@ 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 public class WebConfig implements WebFluxConfigurer { @@ -117,7 +115,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -138,7 +136,7 @@ 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 public class WebConfig implements WebFluxConfigurer { @@ -148,13 +146,13 @@ Java:: DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); registrar.setUseIsoFormat(true); registrar.registerFormatters(registry); - } + } } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -173,7 +171,6 @@ and the `FormattingConversionServiceFactoryBean` for more information on when to use `FormatterRegistrar` implementations. - [[webflux-config-validation]] == Validation [.small]#xref:web/webmvc/mvc-config/validation.adoc[See equivalent in the Servlet stack]# @@ -190,7 +187,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 implements WebFluxConfigurer { @@ -205,7 +202,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -225,7 +222,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 { @@ -240,7 +237,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class MyController { @@ -253,10 +250,9 @@ Kotlin:: ---- ====== - 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 config. - +mark it with `@Primary`, or mark the one declared in the MVC config with `@Fallback`, in +order to avoid conflict. [[webflux-config-content-negotiation]] @@ -273,7 +269,7 @@ 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 public class WebConfig implements WebFluxConfigurer { @@ -287,7 +283,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -300,7 +296,6 @@ Kotlin:: ====== - [[webflux-config-message-codecs]] == HTTP message codecs [.small]#xref:web/webmvc/mvc-config/message-converters.adoc[See equivalent in the Servlet stack]# @@ -311,7 +306,7 @@ 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 public class WebConfig implements WebFluxConfigurer { @@ -325,7 +320,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -340,21 +335,8 @@ Kotlin:: `ServerCodecConfigurer` provides a set of default readers and writers. You can use it to add more readers and writers, customize the default ones, or replace the default ones completely. -For Jackson JSON and XML, consider using -{spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`], -which customizes Jackson's default properties with the following ones: - -* {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: - -* {jackson-github-org}/jackson-datatype-joda[`jackson-datatype-joda`]: Support for Joda-Time types. -* {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. - - +For Jackson, consider using a Jackson format-specific builder like `JsonMapper.Builder` to configure Jackson's default +properties. [[webflux-config-view-resolvers]] == View Resolvers @@ -366,7 +348,7 @@ 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 public class WebConfig implements WebFluxConfigurer { @@ -380,7 +362,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -400,7 +382,7 @@ underlying FreeMarker view technology): ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class WebConfig implements WebFluxConfigurer { @@ -424,7 +406,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -449,7 +431,7 @@ 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 public class WebConfig implements WebFluxConfigurer { @@ -465,7 +447,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -487,7 +469,7 @@ 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 public class WebConfig implements WebFluxConfigurer { @@ -497,7 +479,7 @@ Java:: public void configureViewResolvers(ViewResolverRegistry registry) { registry.freeMarker(); - Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(); + JacksonJsonEncoder encoder = new JacksonJsonEncoder(); registry.defaultViews(new HttpMessageWriterView(encoder)); } @@ -507,7 +489,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -516,7 +498,7 @@ Kotlin:: override fun configureViewResolvers(registry: ViewResolverRegistry) { registry.freeMarker() - val encoder = Jackson2JsonEncoder() + val encoder = JacksonJsonEncoder() registry.defaultViews(HttpMessageWriterView(encoder)) } @@ -528,7 +510,6 @@ Kotlin:: See xref:web/webflux-view.adoc[View Technologies] for more on the view technologies that are integrated with Spring WebFlux. - [[webflux-config-static-resources]] == Static Resources [.small]#xref:web/webmvc/mvc-config/static-resources.adoc[See equivalent in the Servlet stack]# @@ -547,7 +528,7 @@ the example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class WebConfig implements WebFluxConfigurer { @@ -564,7 +545,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -596,7 +577,7 @@ 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 public class WebConfig implements WebFluxConfigurer { @@ -614,7 +595,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -647,18 +628,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. [[webflux-config-path-matching]] @@ -669,53 +648,90 @@ You can customize options related to path matching. For details on the individua {spring-framework-api}/web/reactive/config/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. The following example shows how to use `PathMatchConfigurer`: +include-code::./WebConfig[] + +[TIP] +==== +Spring WebFlux relies on a parsed representation of the request path called +`RequestPath` for access to decoded path segment values, with semicolon content removed +(that is, path or matrix variables). That means, unlike in Spring MVC, you need not indicate +whether to decode the request path nor whether to remove semicolon content for +path matching purposes. + +Spring WebFlux also does not support suffix pattern matching, unlike in Spring MVC, where we +are also xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-suffix-pattern-match[recommend] moving away from +reliance on it. +==== + + +[[webflux-config-api-version]] +== API Version +[.small]#xref:web/webmvc/mvc-config/api-version.adoc[See equivalent in the Servlet stack]# + +To enable API versioning, use the `ApiVersionConfigurer` callback of `WebFluxConfigurer`: + [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim"] ---- @Configuration - public class WebConfig implements WebFluxConfigurer { + public class WebConfiguration implements WebFluxConfigurer { @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - configurer.addPathPrefix( - "/api", HandlerTypePredicate.forAnnotation(RestController.class)); + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer.useRequestHeader("API-Version"); } } ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim"] ---- @Configuration - class WebConfig : WebFluxConfigurer { + class WebConfiguration : WebFluxConfigurer { - @Override - fun configurePathMatch(configurer: PathMatchConfigurer) { - configurer.addPathPrefix( - "/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) + override fun configureApiVersioning(configurer: ApiVersionConfigurer) { + configurer.useRequestHeader("API-Version") } } ---- ====== -[TIP] -==== -Spring WebFlux relies on a parsed representation of the request path called -`RequestPath` for access to decoded path segment values, with semicolon content removed -(that is, path or matrix variables). That means, unlike in Spring MVC, you need not indicate -whether to decode the request path nor whether to remove semicolon content for -path matching purposes. +You can resolve the version through one of the built-in options listed below, or +alternatively use a custom `ApiVersionResolver`: -Spring WebFlux also does not support suffix pattern matching, unlike in Spring MVC, where we -are also xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-suffix-pattern-match[recommend] moving away from -reliance on it. -==== +- Request header +- Request parameter +- Path segment +- Media type parameter + +To resolve from a path segment, you need to specify the index of the path segment expected +to contain the version. The path segment must be declared as a URI variable, e.g. +"/\{version}", "/api/\{version}", etc. where the actual name is not important. +As the version is typically at the start of the path, consider configuring it externally +as a common path prefix for all handlers through the +xref:web/webflux/config.adoc#webflux-config-path-matching[Path Matching] options. +By default, the version is parsed with `SemanticVersionParser`, but you can also configure +a custom xref:web/webflux-versioning.adoc#webflux-versioning-parser[ApiVersionParser]. +Supported versions are transparently detected from versions declared in request mappings +for convenience, but you can turn that off through a flag in the WebFlux config, and +consider only the versions configured explicitly in the config as supported. +Requests with a version that is not supported are rejected with +`InvalidApiVersionException` resulting in a 400 response. + +You can set an `ApiVersionDeprecationHandler` to send information about deprecated +versions to clients. The built-in standard handler can set "Deprecation", "Sunset", and +"Link" headers based on https://datatracker.ietf.org/doc/html/rfc9745[RFC 9745] and +https://datatracker.ietf.org/doc/html/rfc8594[RFC 8594]. + +Once API versioning is configured, you can begin to map requests to +xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[controller methods] +according to the request version. [[webflux-config-blocking-execution]] @@ -732,7 +748,7 @@ as follows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class WebConfig implements WebFluxConfigurer { @@ -747,7 +763,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -766,8 +782,6 @@ By default, controller methods whose return type is not recognized by the config method predicate via `BlockingExecutionConfigurer`. - - [[webflux-config-websocket-service]] == WebSocketService @@ -784,7 +798,7 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Configuration public class WebConfig implements WebFluxConfigurer { @@ -800,7 +814,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : WebFluxConfigurer { @@ -817,8 +831,6 @@ Kotlin:: ====== - - [[webflux-config-advanced-java]] == Advanced Configuration Mode [.small]#xref:web/webmvc/mvc-config/advanced-java.adoc[See equivalent in the Servlet stack]# @@ -837,7 +849,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 { @@ -848,7 +860,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class WebConfig : DelegatingWebFluxConfiguration { @@ -861,7 +873,3 @@ Kotlin:: You can keep existing methods in `WebConfig`, but you can now also override bean declarations from the base class and still have any number of other `WebMvcConfigurer` implementations on the classpath. - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller.adoc index 5db87830f5a5..3fa3597c1b28 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 { @@ -40,6 +40,3 @@ Kotlin:: ====== In the preceding example, the method returns a `String` to be written to the response body. - - - 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 cf77c0ca8409..9308584d4026 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]) @@ -66,4 +66,3 @@ The selectors in the preceding example are evaluated at runtime and may negative performance if used extensively. See the {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 a4435e11d5f5..5c20f2b25b61 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,10 +7,8 @@ `@ExceptionHandler` methods to handle exceptions from controller methods. The following example includes such a handler method: - include-code::./SimpleController[indent=0] - The exception can match against a top-level exception being propagated (that is, a direct `IOException` being thrown) or against the immediate cause within a top-level wrapper exception (for example, an `IOException` wrapped inside an `IllegalStateException`). @@ -30,6 +28,7 @@ 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]# @@ -39,7 +38,6 @@ This allows to refine error responses depending on the media types requested by 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. @@ -56,13 +54,9 @@ the content negotiation during the error handling phase will decide which conten as `@RequestMapping` methods, except the request body might have been consumed already. - [[webflux-ann-exceptionhandler-return-values]] == Return Values [.small]#xref:web/webmvc/mvc-controller/ann-exceptionhandler.adoc#mvc-ann-exceptionhandler-return-values[See equivalent in the Servlet stack]# `@ExceptionHandler` methods support the same xref:web/webflux/controller/ann-methods/return-types.adoc[return values] as `@RequestMapping` methods. - - - 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..5b1371a6e430 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 { @@ -107,8 +107,4 @@ Kotlin:: [[webflux-ann-initbinder-model-design]] -== Model Design -[.small]#xref:web/webmvc/mvc-controller/ann-initbinder.adoc#mvc-ann-initbinder-model-design[See equivalent in the Servlet stack]# - -include::partial$web/web-data-binding-model-design.adoc[] - +NOTE: For more guidance on model design, please see xref:web/webflux/data-binding.adoc[Data Binding]. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods.adoc index 6930c9dbda11..bfa7019959b9 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods.adoc @@ -6,5 +6,3 @@ `@RequestMapping` handler methods have a flexible signature and can choose from a range of supported controller method arguments and return values. - - 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 4c0215ad403b..160ba2926c7c 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 @@ -5,7 +5,7 @@ The following table shows the supported controller method arguments. -Reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other]) are +Reactive types (Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other]) are supported on arguments that require blocking I/O (for example, reading the request body) to be resolved. This is marked in the Description column. Reactive types are not expected on arguments that do not require blocking. @@ -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,8 +114,6 @@ 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 - {spring-framework-api}/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..9c4c0aadb0a1 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> @@ -43,5 +43,3 @@ Kotlin:: Type conversion is applied automatically if the target method parameter type is not `String`. See xref:web/webflux/controller/ann-methods/typeconversion.adoc[Type Conversion]. - - 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..d7be35bc1228 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) { @@ -29,5 +29,3 @@ Kotlin:: } ---- ====== - - 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 ba07fff65a59..a5e1d90b6338 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 @@ -3,6 +3,7 @@ Spring offers support for the Jackson JSON library. + [[webflux-ann-jsonview]] == JSON Views [.small]#xref:web/webmvc/mvc-controller/ann-methods/jackson.adoc[See equivalent in the Servlet stack]# @@ -17,7 +18,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 +60,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController class UserController { @@ -83,6 +84,3 @@ Kotlin:: 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 02d4555997dd..b68c9f200dd3 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 @@ -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 @@ -156,5 +156,3 @@ Kotlin:: } ---- ====== - - 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 45977c0b9e65..1f26694ac54c 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 @@ -39,7 +43,7 @@ request parameters. Argument names are determined through runtime-retained param names in the bytecode. By default, both constructor and property -xref:core/validation/beans-beans.adoc#beans-binding[data binding] are applied. However, +xref:core/validation/data-binding.adoc[data binding] are applied. However, model object design requires careful consideration, and for security reasons it is recommended either to use an object tailored specifically for web binding, or to apply constructor binding only. If property binding must still be used, then _allowedFields_ @@ -54,11 +58,11 @@ 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 { - private final String firstName; + private final String firstName; public Account(@BindParam("first-name") String firstName) { this.firstName = firstName; @@ -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,7 @@ 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 -{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty] +{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` 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..c886714b8e09 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,13 +13,13 @@ as the following example shows: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- class MyForm { private String name; - private MultipartFile file; + private FilePart file; // ... @@ -38,11 +38,11 @@ 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) + private val name: String, + private val file: FilePart) @Controller class FileUploadController { @@ -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,11 +100,11 @@ 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> - @RequestPart("file-data") FilePart file): String { // <2> + fun handle(@RequestPart("meta-data") metadata: Part, // <1> + @RequestPart("file-data") file: FilePart): String { // <2> // ... } ---- @@ -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,11 +166,11 @@ 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 { - // ... + fun handle(@Valid @RequestPart("meta-data") metadata: Mono): String { + // use one of the onError* operators... } ---- ====== @@ -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,10 +199,10 @@ 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> + fun handle(@RequestBody parts: Mono>): String { // <1> // ... } ---- @@ -210,6 +210,7 @@ Kotlin:: ====== -- + [[partevent]] == `PartEvent` @@ -226,87 +227,7 @@ when uploading. If the file is large enough to be split across multiple buffers, For example: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @PostMapping("/") - public void handle(@RequestBody Flux allPartsEvents) { <1> - allPartsEvents.windowUntil(PartEvent::isLast) <2> - .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { <3> - if (signal.hasValue()) { - PartEvent event = signal.get(); - if (event instanceof FormPartEvent formEvent) { <4> - String value = formEvent.value(); - // handle form field - } - else if (event instanceof FilePartEvent fileEvent) { <5> - String filename = fileEvent.filename(); - Flux contents = partEvents.map(PartEvent::content); <6> - // handle file upload - } - else { - return Mono.error(new RuntimeException("Unexpected event: " + event)); - } - } - else { - return partEvents; // either complete or error signal - } - })); - } ----- -<1> Using `@RequestBody`. -<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be -followed by additional events belonging to subsequent parts. -This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to -split events from all parts into windows that each belong to a single part. -<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or -file upload. -<4> Handling the form field. -<5> Handling the file upload. -<6> The body contents must be completely consumed, relayed, or released to avoid memory leaks. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @PostMapping("/") - fun handle(@RequestBody allPartsEvents: Flux) = { // <1> - allPartsEvents.windowUntil(PartEvent::isLast) <2> - .concatMap { - it.switchOnFirst { signal, partEvents -> <3> - if (signal.hasValue()) { - val event = signal.get() - if (event is FormPartEvent) { <4> - val value: String = event.value(); - // handle form field - } else if (event is FilePartEvent) { <5> - val filename: String = event.filename(); - val contents: Flux = partEvents.map(PartEvent::content); <6> - // handle file upload - } else { - return Mono.error(RuntimeException("Unexpected event: " + event)); - } - } else { - return partEvents; // either complete or error signal - } - } - } -} ----- -<1> Using `@RequestBody`. -<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be -followed by additional events belonging to subsequent parts. -This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to -split events from all parts into windows that each belong to a single part. -<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or -file upload. -<4> Handling the form field. -<5> Handling the file upload. -<6> The body contents must be completely consumed, relayed, or released to avoid memory leaks. -====== +include-code::./PartEventController[tag=snippet,indent=0] Received part events can also be relayed to another service by using the `WebClient`. See xref:web/webflux-webclient/client-body.adoc#webflux-client-body-multipart[Multipart Data]. 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..5d473629fd67 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> @@ -31,5 +31,3 @@ Kotlin:: ---- <1> Using `@RequestAttribute`. ====== - - 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..d6c38521aa52 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) { @@ -118,4 +118,3 @@ Kotlin:: If method validation applies because other parameters have `@Constraint` annotations, then `HandlerMethodValidationException` is raised instead. For more details, see the section on xref:web/webflux/controller/ann-validation.adoc[Validation]. - 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..fa6304c61c6a 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( @@ -63,5 +63,3 @@ TIP: Built-in support is available for converting a comma-separated string into array or collection of strings or other types known to the type conversion system. For example, a method parameter annotated with `@RequestHeader("Accept")` may be of type `String` but also of `String[]` or `List`. - - 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 372c4bfaa6be..adc12950032b 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,8 +74,6 @@ 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 -{spring-framework-api}/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..8df9620441d0 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 @@ -38,13 +38,12 @@ than a meta-annotation marked with `@Controller` and `@ResponseBody`. `@ResponseBody` supports reactive types, which means you can return Reactor or RxJava types and have the asynchronous values they produce rendered to the response. -For additional details, see xref:web/webflux/reactive-spring.adoc#webflux-codecs-streaming[Streaming] and -xref:web/webflux/reactive-spring.adoc#webflux-codecs-jackson[JSON rendering]. +For additional details, see xref:web/webflux/reactive-spring.adoc#webflux-codecs-streaming[Streaming] +and xref:web/webflux/reactive-spring.adoc#webflux-codecs-jackson[JSON rendering]. You can combine `@ResponseBody` methods with JSON serialization views. See xref:web/webflux/controller/ann-methods/jackson.adoc[Jackson JSON] for details. -You can use the xref:web/webflux/config.adoc#webflux-config-message-codecs[HTTP message codecs] option of the xref:web/webflux/dispatcher-handler.adoc#webflux-framework-config[WebFlux Config] to -configure or customize message writing. - - +You can use the xref:web/webflux/config.adoc#webflux-config-message-codecs[HTTP message codecs] +option of the xref:web/webflux/dispatcher-handler.adoc#webflux-framework-config[WebFlux Config] +to configure or customize message writing. 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..f7159a0754c8 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 @@ -3,13 +3,14 @@ [.small]#xref:web/webmvc/mvc-controller/ann-methods/responseentity.adoc[See equivalent in the Servlet stack]# -`ResponseEntity` is like xref:web/webflux/controller/ann-methods/responsebody.adoc[`@ResponseBody`] but with status and headers. For example: +`ResponseEntity` is like xref:web/webflux/controller/ann-methods/responsebody.adoc[`@ResponseBody`] +but with status and headers. For example: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/something") public ResponseEntity handle() { @@ -21,7 +22,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,7 +33,7 @@ Kotlin:: ---- ====== -WebFlux supports using a single value xref:web-reactive.adoc#webflux-reactive-libraries[reactive type] to +WebFlux supports using a single value xref:web/webflux-reactive-libraries.adoc[reactive type] to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive types for the body. This allows a variety of async responses with `ResponseEntity` as follows: @@ -45,5 +46,3 @@ for the body. This allows a variety of async responses with `ResponseEntity` as * `Mono>>` or `Mono>>` are yet another possible, albeit less common alternative. They provide the response status and headers asynchronously first and then the response body, also asynchronously, second. - - 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 7c06be75bf6c..daab2a64679e 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 @@ -13,10 +13,11 @@ 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()`. +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"] |=== @@ -34,11 +35,7 @@ Controllers can then return a `Flux>`; Reactor provides a dedicated oper | `HttpHeaders` | For returning a response with headers and no body. -| `ErrorResponse` -| To render an RFC 9457 error response with details in the body, - see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] - -| `ProblemDetail` +| `ErrorResponse`, `ProblemDetail` | To render an RFC 9457 error response with details in the body, see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] @@ -68,6 +65,10 @@ Controllers can then return a `Flux>`; Reactor provides a dedicated oper | `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`, @@ -86,8 +87,6 @@ Controllers can then return a `Flux>`; Reactor provides a dedicated oper | 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 - {spring-framework-api}/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..a3df7ee634d6 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> @@ -38,5 +38,3 @@ For use cases that require adding or removing session attributes, consider injec For temporary storage of model attributes in the session as part of a controller workflow, consider using `SessionAttributes`, as described in xref:web/webflux/controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`]. - - 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..5dd729b04fb4 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> @@ -92,5 +92,3 @@ Kotlin:: <1> Using the `@SessionAttributes` annotation. <2> Using a `SessionStatus` variable. ====== - - diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/typeconversion.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/typeconversion.adoc index ca61ec37c5c2..d81513a425bf 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/typeconversion.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/typeconversion.adoc @@ -10,13 +10,12 @@ can require type conversion if the argument is declared as something other than For such cases, type conversion is automatically applied based on the configured converters. By default, simple types (such as `int`, `long`, `Date`, and others) are supported. Type conversion -can be customized through a `WebDataBinder` (see xref:web/webflux/controller/ann-initbinder.adoc[`DataBinder`]) or by registering -`Formatters` with the `FormattingConversionService` (see xref:core/validation/format.adoc[Spring Field Formatting]). +can be customized through a `WebDataBinder` (see xref:web/webflux/controller/ann-initbinder.adoc[`DataBinder`]) +or by registering `Formatters` with the `FormattingConversionService` (see +xref:core/validation/format.adoc[Spring Field Formatting]). A practical issue in type conversion is the treatment of an empty String source value. Such a value is treated as missing if it becomes `null` as a result of type conversion. This can be the case for `Long`, `UUID`, and other target types. If you want to allow `null` to be injected, either use the `required` flag on the argument annotation, or declare the argument as `@Nullable`. - - 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 fab54b23073a..fd9b033ef8d9 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 @@ -5,9 +5,9 @@ You can use the `@ModelAttribute` annotation: -* On a xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[method argument] in `@RequestMapping` methods -to create or access an Object from the model and to bind it to the request through a -`WebDataBinder`. +* On a xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[method argument] +in `@RequestMapping` methods to create or access an Object from the model and to bind it +to the request through a `WebDataBinder`. * As a method-level annotation in `@Controller` or `@ControllerAdvice` classes, helping to initialize the model prior to any `@RequestMapping` method invocation. * On a `@RequestMapping` method to mark its return value as a model attribute. @@ -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 { @@ -89,12 +89,12 @@ 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) { - Mono accountMono = accountRepository.findAccount(number); - model.addAttribute("account", accountMono); + Mono accountMono = accountRepository.findAccount(number); + model.addAttribute("account", accountMono); } @PostMapping("/accounts") @@ -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") @@ -159,6 +159,3 @@ Kotlin:: } ---- ====== - - - 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 09b30a4a43cf..01f0e23ca71c 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 @@ -5,6 +5,7 @@ This section discusses request mapping for annotated controllers. + [[webflux-ann-requestmapping-annotation]] == `@RequestMapping` @@ -40,7 +41,7 @@ The following example uses type and method level mappings: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/persons") @@ -61,7 +62,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/persons") @@ -88,39 +89,7 @@ Kotlin:: You can map requests by using glob patterns and wildcards: -[cols="2,3,5"] -|=== -|Pattern |Description |Example - -| `+?+` -| Matches one character -| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+` - -| `+*+` -| Matches zero or more characters within a path segment -| `+"/resources/*.png"+` matches `+"/resources/file.png"+` - -`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+` - -| `+**+` -| Matches zero or more path segments until the end of the path -| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+` - -`+"/resources/**/file.png"+` is invalid as `+**+` is only allowed at the end of the path. - -| `+{name}+` -| Matches a path segment and captures it as a variable named "name" -| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+` - -| `+{name:[a-z]+}+` -| Matches the regexp `+"[a-z]+"+` as a path variable named "name" -| `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+` - -| `+{*path}+` -| Matches zero or more path segments until the end of the path and captures it as a variable named "path" -| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+` - -|=== +include::partial$web/uri-patterns.adoc[leveloffset=+1] Captured URI variables can be accessed with `@PathVariable`, as the following example shows: @@ -129,7 +98,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) { @@ -139,7 +108,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 { @@ -156,7 +125,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> @@ -173,7 +142,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/owners/{ownerId}") // <1> @@ -213,7 +182,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) { @@ -223,7 +192,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) { @@ -233,10 +202,13 @@ Kotlin:: ====== -- -URI path patterns can also have embedded `${...}` placeholders that are resolved on startup -through `PropertySourcesPlaceholderConfigurer` against local, system, environment, and -other property sources. You can use this to, for example, parameterize a base URL based on -some external configuration. +URI path patterns can also have: + +- Embedded `${...}` placeholders that are resolved on startup via +`PropertySourcesPlaceholderConfigurer` against local, system, environment, and +other property sources. This is useful, for example, to parameterize a base URL based on +external configuration. +- SpEL expressions `#{...}`. NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support. Both classes are located in `spring-web` and are expressly designed for use with HTTP URL @@ -274,7 +246,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) { @@ -284,7 +256,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) { @@ -315,7 +287,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 @@ -326,7 +298,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 @@ -343,7 +315,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`. @@ -359,7 +331,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) { @@ -370,7 +342,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) { @@ -386,7 +358,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) { @@ -397,7 +369,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) { @@ -408,6 +380,92 @@ Kotlin:: ====== +[[webflux-ann-requestmapping-version]] +== API Version +[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[See equivalent in the Servlet stack]# + +There is no standard way to specify an API version, so when you enable API versioning +in the xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config] you need +to specify how to resolve the version. The WebFlux Config creates an +xref:web/webflux-versioning.adoc#webflux-versioning-strategy[ApiVersionStrategy] that in turn +is used to map requests. + +Once API versioning is enabled, you can begin to map requests with versions. +The `@RequestMapping` `version` attribute supports the following: + +- Fixed version ("1.2") -- matches the given version only +- Baseline version ("1.2+") -- matches the given and +xref:web/webflux/config.adoc#webflux-config-api-version[supported versions] above +- No value -- matches any version, but is superseded by a more specific version match + +If multiple controller methods have a version less than or equal to the request version, +the highest of those, and closest to the request version, is the one considered, +in effect superseding the rest. + +To illustrate this, consider the following mappings: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @RestController + @RequestMapping("/account/{id}") + public class AccountController { + + @GetMapping // <1> + public Account getAccount() { + } + + @GetMapping(version = "1.1") // <2> + public Account getAccount1_1() { + } + + @GetMapping(version = "1.2+") // <3> + public Account getAccount1_2() { + } + + @GetMapping(version = "1.5") // <4> + public Account getAccount1_5() { + } + } +---- +<1> match any version +<2> match version 1.1 +<3> match version 1.2 and above +<4> match version 1.5 +====== + +For request with version `"1.3"`: + +- (1) matches as it matches any version +- (2) does not match +- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match +- (4) is higher and does not match + +NOTE: Version 1.3 must be present in the mappings, or be +xref:web/webflux/config.adoc#webflux-config-api-version[configured as supported]. + +For request with version `"1.5"`: + +- (1) matches as it matches any version +- (2) does not match +- (3) matches as it matches 1.2 and above +- (4) matches and is *chosen* as the highest match + +A request with version `"1.6"` does not have a match. (1) and (3) do match, but are +superseded by (4), which allows only a strict match, and therefore does not match. +In this scenario, a `NotAcceptableApiVersionException` results in a 400 response. + +Controller methods without a version are intended to support clients created before a +versioned alternative was introduced. Therefore, even though an unversioned controller +method is considered a match for any version, it is in fact given the lowest priority, +and is effectively superseded by any alternative controller method with a version. + +See xref:web/webflux-versioning.adoc[API Versioning] for more details on underlying +infrastructure and support for API Versioning. + [[webflux-ann-requestmapping-head-options]] == HTTP HEAD, OPTIONS @@ -469,7 +527,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 { @@ -495,7 +553,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration class MyConfig { @@ -518,22 +576,20 @@ Kotlin:: ====== - [[webflux-ann-httpexchange-annotation]] == `@HttpExchange` [.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. +While the main purpose of `@HttpExchange` is for an HTTP Service +xref:integration/rest-clients.adoc#rest-http-service-client[client with a generated proxy], +the HTTP Service 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 Service 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: @@ -541,7 +597,7 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @HttpExchange("/persons") interface PersonService { @@ -569,7 +625,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @HttpExchange("/persons") interface PersonService { @@ -604,5 +660,5 @@ 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:integration/rest-clients.adoc#rest-http-service-client-method-parameters[@HttpExchange] and xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping]. 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 41c753c53967..67fb1256a889 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 @@ -7,21 +7,25 @@ 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: -1. xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], +1. Java Bean Validation is applied individually to an +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] 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 +xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] method parameter +annotated with `@jakarta.validation.Valid` or Spring's `@Validated` so long as +it is a command object rather than a container such as `Map` or `Collection`, it does not +have `Errors` or `BindingResult` immediately after in the method signature, and does not +otherwise require method validation (see next). `WebExchangeBindException` is the +exception raised when validating a method parameter individually. + +2. Java Bean Validation is applied to the method when `@Constraint` annotations such as +`@Min`, `@NotBlank` and others are declared directly on method parameters, or on the +method for the return value, and it supersedes any validation that would be applied +otherwise to a method parameter individually because method validation covers both +method parameter constraints and nested constraints via `@Valid`. +`HandlerMethodValidationException` is the exception raised validation is applied +to the method. + +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 @@ -39,7 +43,7 @@ method parameters with an `Errors` immediately after. If there are validation er 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. @@ -49,15 +53,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: @@ -65,7 +69,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 = ... ; @@ -95,7 +99,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // HandlerMethodValidationException val ex @@ -104,21 +108,21 @@ Kotlin:: override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) { // ... - } + } override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) { // ... - } + } override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) { // ... - } + } // ... override fun other(result: ParameterValidationResult) { // ... - } + } }) ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc index 602d2b591a75..2c4427d09e04 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc @@ -16,7 +16,7 @@ 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> @@ -29,7 +29,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration @ComponentScan("org.example.web") // <1> @@ -47,7 +47,6 @@ every method inherits the type-level `@ResponseBody` annotation and, therefore, directly to the response body versus view resolution and rendering with an HTML template. - [[webflux-ann-requestmapping-proxying]] == AOP Proxies [.small]#xref:web/webmvc/mvc-controller/ann.adoc#mvc-ann-requestmapping-proxying[See equivalent in the Servlet stack]# @@ -67,7 +66,3 @@ NOTE: Keep in mind that as of 6.0, with interface proxying, Spring WebFlux no lo controllers based solely on a type-level `@RequestMapping` annotation on the interface. Please, enable class based proxying, or otherwise the interface must also have an `@Controller` annotation. - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webflux/data-binding.adoc b/framework-docs/modules/ROOT/pages/web/webflux/data-binding.adoc new file mode 100644 index 000000000000..d41968aff165 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webflux/data-binding.adoc @@ -0,0 +1,36 @@ +[[webflux-data-binding]] += Data Binding +:page-section-summary-toc: 1 + +[.small]#xref:web/webmvc/mvc-data-binding.adoc[See equivalent in the Servlet stack]# + +Data binding is a mechanism that binds string parameters onto an object graph with type conversion. +It is a core mechanism of the Spring Framework that helps with application configuration. +In web applications it makes it easy to access query parameters and form data through richly typed objects rather than through maps of string values. + +To learn more about the data binding mechanism, including constructor and setter binding, property name syntax, type conversion, +and more, see xref:core/validation/data-binding.adoc[Data binding] in the Core Technologies section. + +For annotated controllers, data binding applies to a +xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute] method argument. +For functional endpoints, use the `bind` method of xref:web/webflux-functional.adoc#webflux-fn-request[ServerRequest]. + +TIP: For browser applications with annotated controllers, you can use +xref:web/webflux/controller/ann-modelattrib-methods.adoc[@ModelAttribute methods] +to initialize additional model attributes for use in rendered views. + +Each request uses a separate `WebDataBinder` instance. +For annotated controllers, this instance can be customized through +xref:web/webflux/controller/ann-initbinder.adoc[@InitBinder methods] within a controller, or +across controllers through xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice]. +For functional endpoints, use overloaded `ServerRequest.bind` methods. + + + + +[[webflux-data-binding-design]] +== Model Design +[.small]#xref:web/webmvc/mvc-data-binding.adoc#mvc-data-binding-design[See equivalent in the Servlet stack]# + +include::partial$web/web-data-binding-model-design.adoc[] + 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 a621320538ae..f119a721d908 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/dispatcher-handler.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/dispatcher-handler.adoc @@ -13,7 +13,8 @@ It is also designed to be a Spring bean itself and implements `ApplicationContex 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 {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]. +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 +30,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,15 +38,15 @@ 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() ---- ====== -The resulting `HttpHandler` is ready for use with a xref:web/webflux/reactive-spring.adoc#webflux-httphandler[server adapter]. - +The resulting `HttpHandler` is ready for use with a +xref:web/webflux/reactive-spring.adoc#webflux-httphandler[server adapter]. [[webflux-special-bean-types]] @@ -59,7 +60,8 @@ you can customize their properties, extend them, or replace them. The following table lists the special beans detected by the `DispatcherHandler`. Note that there are also some other beans detected at a lower level (see -xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[Special bean types] in the Web Handler API). +xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[Special bean types] +in the Web Handler API). [[webflux-special-beans-table]] [cols="1,2", options="header"] @@ -89,22 +91,22 @@ xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[Spec |=== - [[webflux-framework-config]] == WebFlux Config [.small]#xref:web/webmvc/mvc-servlet/config.adoc[See equivalent in the Servlet stack]# Applications can declare the infrastructure beans (listed under xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[Web Handler API] and -xref:web/webflux/dispatcher-handler.adoc#webflux-special-bean-types[`DispatcherHandler`]) that are required to process requests. -However, in most cases, the xref:web/webflux/dispatcher-handler.adoc#webflux-framework-config[WebFlux Config] is the best starting point. It declares the -required beans and provides a higher-level configuration callback API to customize it. +xref:web/webflux/dispatcher-handler.adoc#webflux-special-bean-types[`DispatcherHandler`]) +that are required to process requests. However, in most cases, the +xref:web/webflux/dispatcher-handler.adoc#webflux-framework-config[WebFlux Config] +is the best starting point. It declares the required beans and provides a higher-level +configuration callback API to customize it. NOTE: Spring Boot relies on the WebFlux config to configure Spring WebFlux and also provides many extra convenient options. - [[webflux-dispatcher-handler-sequence]] == Processing [.small]#xref:web/webmvc/mvc-servlet/sequence.adoc[See equivalent in the Servlet stack]# @@ -118,14 +120,14 @@ exposes the return value from the execution as `HandlerResult`. processing by writing to the response directly or by using a view to render. - [[webflux-resulthandling]] == Result Handling The return value from the invocation of a handler, through a `HandlerAdapter`, is wrapped as a `HandlerResult`, along with some additional context, and passed to the first `HandlerResultHandler` that claims support for it. The following table shows the available -`HandlerResultHandler` implementations, all of which are declared in the xref:web/webflux/dispatcher-handler.adoc#webflux-framework-config[WebFlux Config]: +`HandlerResultHandler` implementations, all of which are declared in the +xref:web/webflux/dispatcher-handler.adoc#webflux-framework-config[WebFlux Config]: [cols="1,2,1", options="header"] |=== @@ -155,7 +157,6 @@ as a `HandlerResult`, along with some additional context, and passed to the firs |=== - [[webflux-dispatcher-exceptions]] == Exceptions [.small]#xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[See equivalent in the Servlet stack]# @@ -170,23 +171,23 @@ 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. - [[webflux-viewresolution]] == View Resolution [.small]#xref:web/webmvc/mvc-servlet/viewresolver.adoc[See equivalent in the Servlet stack]# 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` -instance. The `View` is then used to render the response. +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` 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 @@ -206,7 +207,7 @@ was not provided (for example, model attribute was returned) or an async return 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 -{spring-framework-api}/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 {spring-framework-api}/core/Conventions.html[conventions], unless a handler method `@ModelAttribute` annotation is present. @@ -223,7 +224,6 @@ dedicated configuration API for view resolution. See xref:web/webflux-view.adoc[View Technologies] for more on the view technologies integrated with Spring WebFlux. - [[webflux-redirecting-redirect-prefix]] === Redirecting [.small]#xref:web/webmvc/mvc-servlet/viewresolver.adoc#mvc-redirecting-redirect-prefix[See equivalent in the Servlet stack]# @@ -238,6 +238,8 @@ 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 @@ -252,7 +254,3 @@ In order to support media types such as JSON and XML, Spring WebFlux provides xref:web/webflux/reactive-spring.adoc#webflux-codecs[HttpMessageWriter]. Typically, you would configure these as default views through the xref:web/webflux/config.adoc#webflux-config-view-resolvers[WebFlux Configuration]. Default views are always selected and used if they match the requested media type. - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc b/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc index 1b5a9e643a7e..c9a5f19080b1 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc @@ -4,6 +4,6 @@ [.small]#xref:web/webmvc/mvc-http2.adoc[See equivalent in the Servlet stack]# -HTTP/2 is supported with Reactor Netty, Tomcat, Jetty, and Undertow. However, there are +HTTP/2 is supported with Reactor Netty, Tomcat, and Jetty. However, there are considerations related to server configuration. For more details, see the {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 b03cefb04bbe..e11a2e68df85 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc @@ -20,7 +20,6 @@ composition of asynchronous logic. At the programming-model level, Java 8 enable WebFlux to offer functional web endpoints alongside annotated controllers. - [[webflux-why-reactive]] == Define "`Reactive`" @@ -53,14 +52,13 @@ The purpose of Reactive Streams is only to establish the mechanism and a boundar If a publisher cannot slow down, it has to decide whether to buffer, drop, or fail. - [[webflux-reactive-api]] == Reactive API Reactive Streams plays an important role for interoperability. It is of interest to libraries and infrastructure components but less useful as an application API, because it is too 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. +compose async logic -- similar to the Java `Stream` API but not only for collections. This is the role that reactive libraries play. {reactor-github-org}/reactor[Reactor] is the reactive library of choice for @@ -79,34 +77,33 @@ as input, adapts it to a Reactor type internally, uses that, and returns either `Flux` or a `Mono` as output. So, you can pass any `Publisher` as input and you can apply operations on the output, but you need to adapt the output for use with another reactive library. Whenever feasible (for example, annotated controllers), WebFlux adapts transparently to the use -of RxJava or another reactive library. See xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries] for more details. +of RxJava or another reactive library. See xref:web/webflux-reactive-libraries.adoc[Reactive Libraries] for more details. NOTE: In addition to Reactive APIs, WebFlux can also be used with xref:languages/kotlin/coroutines.adoc[Coroutines] APIs in Kotlin which provides a more imperative style of programming. The following Kotlin code samples will be provided with Coroutines APIs. - [[webflux-programming-models]] == Programming Models The `spring-web` module contains the reactive foundation that underlies Spring WebFlux, -including HTTP abstractions, Reactive Streams xref:web/webflux/reactive-spring.adoc#webflux-httphandler[adapters] for supported -servers, xref:web/webflux/reactive-spring.adoc#webflux-codecs[codecs], and a core xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API] comparable to +including HTTP abstractions, Reactive Streams xref:web/webflux/reactive-spring.adoc#webflux-httphandler[adapters] +for supported servers, xref:web/webflux/reactive-spring.adoc#webflux-codecs[codecs], and a core +xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API] comparable to the Servlet API but with non-blocking contracts. On that foundation, Spring WebFlux provides a choice of two programming models: -* xref:web/webflux/controller.adoc[Annotated Controllers]: Consistent with Spring MVC and based on the same annotations -from the `spring-web` module. Both Spring MVC and WebFlux controllers support reactive +* xref:web/webflux/controller.adoc[Annotated Controllers]: Consistent with Spring MVC and based on the +same annotations 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 -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 -annotations and being called back. - +* 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 annotations and being called back. [[webflux-framework-choice]] @@ -130,11 +127,11 @@ You have maximum choice of libraries, since, historically, most are blocking. * If you are already shopping for a non-blocking web stack, Spring WebFlux offers the same execution model benefits as others in this space and also provides a choice of servers -(Netty, Tomcat, Jetty, Undertow, and Servlet containers), a choice of programming models +(Netty, Tomcat, Jetty, and Servlet containers), a choice of programming models (annotated controllers and functional web endpoints), and a choice of reactive libraries (Reactor, RxJava, or other). -* If you are interested in a lightweight, functional web framework for use with Java 8 lambdas +* If you are interested in a lightweight, functional web framework for use with Java or Kotlin, you can use the Spring WebFlux functional web endpoints. That can also be a good choice for smaller applications or microservices with less complex requirements that can benefit from greater transparency and control. @@ -151,7 +148,7 @@ RxJava to perform blocking calls on a separate thread but you would not be makin most of a non-blocking web stack. * If you have a Spring MVC application with calls to remote services, try the reactive `WebClient`. -You can return reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other]) +You can return reactive types (Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other]) directly from Spring MVC controller methods. The greater the latency per call or the interdependency among calls, the more dramatic the benefits. Spring MVC controllers can call other reactive components too. @@ -164,12 +161,11 @@ unsure what benefits to look for, start by learning about how non-blocking I/O w (for example, concurrency on single-threaded Node.js) and its effects. - [[webflux-server-choice]] == Servers Spring WebFlux is supported on Tomcat, Jetty, Servlet containers, as well as on -non-Servlet runtimes such as Netty and Undertow. All servers are adapted to a low-level, +non-Servlet runtimes such as Netty. All servers are adapted to a low-level, xref:web/webflux/reactive-spring.adoc#webflux-httphandler[common API] so that higher-level xref:web/webflux/new-framework.adoc#webflux-programming-models[programming models] can be supported across servers. @@ -179,7 +175,7 @@ xref:web/webflux/dispatcher-handler.adoc#webflux-framework-config[WebFlux infras lines of code. Spring Boot has a WebFlux starter that automates these steps. By default, the starter uses -Netty, but it is easy to switch to Tomcat, Jetty, or Undertow by changing your +Netty, but it is easy to switch to Tomcat, or Jetty by changing your Maven or Gradle dependencies. Spring Boot defaults to Netty, because it is more widely used in the asynchronous, non-blocking space and lets a client and a server share resources. @@ -192,9 +188,6 @@ adapter. It is not exposed for direct use. NOTE: It is strongly advised not to map Servlet filters or directly manipulate the Servlet API in the context of a WebFlux application. For the reasons listed above, mixing blocking I/O and non-blocking I/O in the same context will cause runtime issues. -For Undertow, Spring WebFlux uses Undertow APIs directly without the Servlet API. - - [[webflux-performance]] == Performance @@ -212,7 +205,6 @@ That is where the reactive stack begins to show its strengths, and the differenc dramatic. - [[webflux-concurrency-model]] == Concurrency Model @@ -231,7 +223,6 @@ TIP: "`To scale`" and "`small number of threads`" may sound contradictory, but t current thread (and rely on callbacks instead) means that you do not need extra threads, as there are no blocking calls to absorb. - [[invoking-a-blocking-api]] === Invoking a Blocking API @@ -283,7 +274,3 @@ you need to use server-specific configuration APIs, or, if you use Spring Boot, check the Spring Boot configuration options for each server. You can xref:web/webflux-webclient/client-builder.adoc[configure] the `WebClient` directly. For all other libraries, see their respective documentation. - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webflux/range.adoc b/framework-docs/modules/ROOT/pages/web/webflux/range.adoc new file mode 100644 index 000000000000..edcd170bd574 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webflux/range.adoc @@ -0,0 +1,23 @@ +[[webflux-range]] += Range Requests +:page-section-summary-toc: 1 + +[.small]#xref:web/webmvc/mvc-range.adoc[See equivalent in the Servlet stack]# + +Spring WebFlux supports https://datatracker.ietf.org/doc/html/rfc9110#section-14[RFC 9110] +range requests. For an overview, see the +https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests[Ranger Requests] +Mozilla guide. + +The `Range` header is parsed and handled transparently in WebFlux when an annotated +controller returns a `Resource` or `ResponseEntity`, or a functional endpoint +xref:web/webflux-functional.adoc#webflux-fn-resources[serves a `Resource`]. `Range` header +support is also transparently handled when serving +xref:web/webflux/config.adoc#webflux-config-static-resources[static resources]. + +TIP: The `Resource` must not be an `InputStreamResource` and with `ResponseEntity`, +the status of the response must be 200. + +The underlying support is in the `HttpRange` class, which exposes methods to parse +`Range` headers and split a `Resource` into a `List` that in turn can be +then written to the response via `ResourceRegionEncoder` and `ResourceHttpMessageWriter`. \ No newline at end of file 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 13d527592cc8..f3fddd58e1b5 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc @@ -7,7 +7,7 @@ applications: * For server request processing there are two levels of support. ** xref:web/webflux/reactive-spring.adoc#webflux-httphandler[HttpHandler]: Basic contract for HTTP request handling with non-blocking I/O and Reactive Streams back pressure, along with adapters for Reactor Netty, -Undertow, Tomcat, Jetty, and any Servlet container. +Tomcat, Jetty, and any Servlet container. ** xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API]: Slightly higher level, general-purpose web API for request handling, on top of which concrete programming models such as annotated controllers and functional endpoints are built. @@ -22,7 +22,6 @@ builds on this basic contract. deserialization of HTTP request and response content. - [[webflux-httphandler]] == `HttpHandler` @@ -41,10 +40,6 @@ The following table describes the supported server APIs: | Netty API | {reactor-github-org}/reactor-netty[Reactor Netty] -| Undertow -| Undertow API -| spring-web: Undertow to Reactive Streams bridge - | Tomcat | Servlet non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[] | spring-web: Servlet non-blocking I/O to Reactive Streams bridge @@ -68,10 +63,6 @@ The following table describes server dependencies (also see |io.projectreactor.netty |reactor-netty -|Undertow -|io.undertow -|undertow-core - |Tomcat |org.apache.tomcat.embed |tomcat-embed-core @@ -81,14 +72,14 @@ The following table describes server dependencies (also see |jetty-server, jetty-servlet |=== -The code snippets below show using the `HttpHandler` adapters with each server API: +The code snippets below show using the `HttpHandler` adapters with each server API. *Reactor Netty* [tabs] ====== 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 +88,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) @@ -105,36 +96,12 @@ Kotlin:: ---- ====== -*Undertow* -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - HttpHandler handler = ... - UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler); - Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build(); - server.start(); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val handler: HttpHandler = ... - val adapter = UndertowHttpHandlerAdapter(handler) - val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build() - server.start() ----- -====== - *Tomcat* [tabs] ====== 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 +118,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,56 +140,55 @@ 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); + JettyCoreHttpHandlerAdapter adapter = new JettyCoreHttpHandlerAdapter(handler); Server server = new Server(); - ServletContextHandler contextHandler = new ServletContextHandler(server, ""); - contextHandler.addServlet(new ServletHolder(servlet), "/"); - contextHandler.start(); + server.setHandler(adapter); ServerConnector connector = new ServerConnector(server); connector.setHost(host); connector.setPort(port); server.addConnector(connector); + server.start(); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val handler: HttpHandler = ... - val servlet = JettyHttpHandlerAdapter(handler) + val adapter = JettyCoreHttpHandlerAdapter(handler) val server = Server() - val contextHandler = ServletContextHandler(server, "") - contextHandler.addServlet(ServletHolder(servlet), "/") - contextHandler.start(); + server.setHandler(adapter) val connector = ServerConnector(server) connector.host = host connector.port = port server.addConnector(connector) + server.start() ---- ====== -*Servlet Container* - -To deploy as a WAR to any Servlet container, you can extend and include -{spring-framework-api}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`] -in the WAR. That class wraps an `HttpHandler` with `ServletHttpHandlerAdapter` and registers -that as a `Servlet`. +TIP: In Spring Framework 6.2, `JettyHttpHandlerAdapter` was deprecated in favor of +`JettyCoreHttpHandlerAdapter`, which integrates directly with Jetty 12 APIs +without a Servlet layer. +To deploy as a WAR to a Servlet container instead, use +{spring-framework-api}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`], +to adapt `HttpHandler` to a `Servlet` via `ServletHttpHandlerAdapter`. [[webflux-web-handler-api]] == `WebHandler` API -The `org.springframework.web.server` package builds on the xref:web/webflux/reactive-spring.adoc#webflux-httphandler[`HttpHandler`] contract +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 {spring-framework-api}/web/server/WebExceptionHandler.html[`WebExceptionHandler`], multiple {spring-framework-api}/web/server/WebFilter.html[`WebFilter`], and a single @@ -305,14 +271,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 +300,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 ---- @@ -363,40 +329,35 @@ to individual parts by name and, hence, requires parsing multipart data in full. By contrast, you can use `@RequestBody` to decode the content to `Flux` without collecting to a `MultiValueMap`. - [[webflux-forwarded-headers]] === Forwarded Headers [.small]#xref:web/webmvc/filters.adoc#filters-forwarded-headers[See equivalent in the Servlet stack]# include::partial$web/forwarded-headers.adoc[] - - [[webflux-forwarded-headers-transformer]] === ForwardedHeaderTransformer -`ForwardedHeaderTransformer` is a component that modifies the host, port, and scheme of -the request, based on forwarded headers, and then removes those headers. If you declare -it as a bean with the name `forwardedHeaderTransformer`, it will be +`ForwardedHeaderTransformer` is a component that modifies the request to match information +from the standard `"Forwarded"` or `"X-Forwarded"` headers, and also removes those headers +to eliminate further impact. If you declare it as a bean with the name +`forwardedHeaderTransformer`, it will be xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[detected] and used. -NOTE: In 5.1 `ForwardedHeaderFilter` was deprecated and superseded by -`ForwardedHeaderTransformer` so forwarded headers can be processed earlier, before the -exchange is created. If the filter is configured anyway, it is taken out of the list of -filters, and `ForwardedHeaderTransformer` is used instead. - - - [[webflux-forwarded-headers-security]] === Security Considerations -There are security considerations for forwarded headers since an application cannot know -if the headers were added by a proxy, as intended, or by a malicious client. This is why -a proxy at the boundary of trust should be configured to remove untrusted forwarded traffic coming -from the outside. You can also configure the `ForwardedHeaderTransformer` with -`removeOnly=true`, in which case it removes but does not use the headers. +Forwarded headers are intended to be set by trusted proxies and never allowed in from the +outside. A proxy at the edge of trust must remove forwarded headers including both the +standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use, +to protect applications which may check both. +When creating `ForwardedHeaderTransformer` you need to specify whether it should use the +standard `"Forwarded"` or `"X-Forwarded"` headers. If needed `"X-Forwarded-Prefix"` +must be enabled separately through a property on the transformer. +`ForwardedHeaderTransformer` can be configured in `removeOnly` mode, in which case it removes +forwarded headers from the request without using them. [[webflux-filters]] == Filters @@ -408,7 +369,6 @@ logic before and after the rest of the processing chain of filters and the targe as declaring it as a Spring bean and (optionally) expressing precedence by using `@Order` on the bean declaration or by implementing `Ordered`. - [[webflux-filters-cors]] === CORS [.small]#xref:web/webmvc/filters.adoc#filters-cors[See equivalent in the Servlet stack]# @@ -419,6 +379,30 @@ 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")`. + +Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash. +This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings. +The filter can remove the trailing slash in one of a couple of ways: + +* respond with an HTTP redirect status that sends clients to the same path without a trailing slash. +* mutate the request to remove the trailing slash. + +Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application: + +include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0] + +Keep in mind the following: + +- the root path `"/"` is excluded from trailing slash handling. +- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will +not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead. + [[webflux-exception-handler]] == Exceptions @@ -450,10 +434,9 @@ 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 @@ -468,7 +451,7 @@ 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`. * {spring-framework-api}/core/io/buffer/DataBuffer.html[`DataBuffer`] abstracts different -byte buffer representations (e.g. Netty `ByteBuf`, `java.nio.ByteBuffer`, etc.) and is +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. @@ -488,35 +471,35 @@ xref:web/webflux/config.adoc#webflux-config-message-codecs[HTTP message codecs]. 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: +The `JacksonJsonDecoder` 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 -the `ObjectMapper` as soon as enough bytes are received for a fully formed object. The +* Each `TokenBuffer` is passed to Jackson's `JsonMapper` to create a higher level object. +* 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 `JsonMapper` 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, JSON Lines, or JSON Text Sequences. -The `Jackson2Encoder` works as follows: +The `JacksonJsonEncoder` works as follows: -* For a single value publisher (e.g. `Mono`), simply serialize it through the -`ObjectMapper`. +* For a single value publisher (for example, `Mono`), simply serialize it through the +`JsonMapper`. * For a multi-value publisher with `application/json`, by default collect the values with `Flux#collectToList()` and then serialize the resulting collection. * For a multi-value publisher with a streaming media type such as -`application/x-ndjson` or `application/stream+x-jackson-smile`, encode, write, and -flush each value individually using a +`application/jsonl`, `application/x-ndjson` or `application/stream+x-jackson-smile`, +encode, write, and flush each value individually using a https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format. Other streaming media types may be registered with the encoder. -* For SSE the `Jackson2Encoder` is invoked per event and the output is flushed to ensure +* For SSE the `JacksonJsonEncoder` is invoked per event and the output is flushed to ensure delivery without delay. [NOTE] ==== -By default both `Jackson2Encoder` and `Jackson2Decoder` do not support elements of type +By default both `JacksonJsonEncoder` and `JacksonJsonDecoder` do not support elements of type `String`. Instead the default assumption is that a string or a sequence of strings represent serialized JSON content, to be rendered by the `CharSequenceEncoder`. If what you need is to render a JSON array from `Flux`, use `Flux#collectToList()` and @@ -532,13 +515,13 @@ encode a `Mono>`. On the server side where form content often needs to be accessed from multiple places, `ServerWebExchange` provides a dedicated `getFormData()` method that parses the content through `FormHttpMessageReader` and then caches the result for repeated access. -See xref:web/webflux/reactive-spring.adoc#webflux-form-data[Form Data] in the xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API] section. +See xref:web/webflux/reactive-spring.adoc#webflux-form-data[Form Data] in the +xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API] section. Once `getFormData()` is used, the original raw content can no longer be read from the request body. For this reason, applications are expected to go through `ServerWebExchange` consistently for access to the cached form data versus reading from the raw request body. - [[webflux-codecs-multipart]] === Multipart @@ -554,13 +537,39 @@ For more information about the `DefaultPartHttpMessageReader`, refer to the On the server side where multipart form content may need to be accessed from multiple places, `ServerWebExchange` provides a dedicated `getMultipartData()` method that parses the content through `MultipartHttpMessageReader` and then caches the result for repeated access. -See xref:web/webflux/reactive-spring.adoc#webflux-multipart[Multipart Data] in the xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API] section. +See xref:web/webflux/reactive-spring.adoc#webflux-multipart[Multipart Data] in the +xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API] section. Once `getMultipartData()` is used, the original raw content can no longer be read from the request body. For this reason applications have to consistently use `getMultipartData()` 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-gson]] +=== Google Gson + +Applications can use the `GsonEncoder` and `GsonDecoder` to serialize and deserialize JSON documents thanks to the https://google.github.io/gson/[Google Gson] library . +This codec supports both JSON media types and the NDJSON format for streaming. + +[NOTE] +==== +`Gson` does not support non-blocking parsing, so the `GsonDecoder` does not support deserializing +to `Flux<*>` types. For example, if this decoder is used for deserializing a JSON stream or even a list of elements +as a `Flux<*>`, an `UnsupportedOperationException` will be thrown at runtime. +Applications should instead focus on deserializing bounded collections and use `Mono>` as target types. +==== [[webflux-codecs-limits]] === Limits @@ -589,19 +598,16 @@ a `maxParts` property to limit the overall number of parts in a multipart reques To configure all three in WebFlux, you'll need to supply a pre-configured instance of `MultipartHttpMessageReader` to `ServerCodecConfigurer`. - - [[webflux-codecs-streaming]] === Streaming [.small]#xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[See equivalent in the Servlet stack]# When streaming to the HTTP response (for example, `text/event-stream`, -`application/x-ndjson`), it is important to send data periodically, in order to +`application/jsonl`, `application/x-ndjson`), it is important to send data periodically, in order to reliably detect a disconnected client sooner rather than later. Such a send could be a comment-only, empty SSE event or any other "no-op" data that would effectively serve as a heartbeat. - [[webflux-codecs-buffers]] === `DataBuffer` @@ -618,7 +624,6 @@ cases please review the information in xref:core/databuffer-codec.adoc[Data Buff especially the section on xref:core/databuffer-codec.adoc#databuffers-using[Using DataBuffer]. - [[webflux-logging]] == Logging [.small]#xref:web/webmvc/mvc-servlet/logging.adoc[See equivalent in the Servlet stack]# @@ -634,7 +639,6 @@ messages may show a different level of detail at `TRACE` vs `DEBUG`. Good logging comes from the experience of using the logs. If you spot anything that does not meet the stated goals, please let us know. - [[webflux-logging-id]] === Log Id @@ -650,7 +654,6 @@ while a fully formatted prefix based on that ID is available from ({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()`. - [[webflux-logging-sensitive-data]] === Sensitive Data [.small]#xref:web/webmvc/mvc-servlet/logging.adoc#mvc-logging-sensitive-data[See equivalent in the Servlet stack]# @@ -664,10 +667,9 @@ 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 class MyConfig implements WebFluxConfigurer { @Override @@ -679,10 +681,9 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Configuration - @EnableWebFlux class MyConfig : WebFluxConfigurer { override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { @@ -698,7 +699,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 +711,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) } @@ -720,7 +721,6 @@ Kotlin:: ---- ====== - [[webflux-logging-appenders]] === Appenders @@ -729,8 +729,6 @@ blocking. While those have their own drawbacks such as potentially dropping mess that could not be queued for logging, they are the best available options currently for use in a reactive, non-blocking application. - - [[webflux-codecs-custom]] === Custom codecs @@ -748,26 +746,25 @@ 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 -> { - CustomDecoder decoder = new CustomDecoder(); - configurer.customCodecs().registerWithDefaultConfig(decoder); + CustomDecoder decoder = new CustomDecoder(); + configurer.customCodecs().registerWithDefaultConfig(decoder); }) .build(); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val webClient = WebClient.builder() .codecs({ configurer -> - val decoder = CustomDecoder() - configurer.customCodecs().registerWithDefaultConfig(decoder) + val decoder = CustomDecoder() + configurer.customCodecs().registerWithDefaultConfig(decoder) }) .build() ---- ====== - diff --git a/framework-docs/modules/ROOT/pages/web/webflux/security.adoc b/framework-docs/modules/ROOT/pages/web/webflux/security.adoc index fcb982d254cb..6e1d056e5cd0 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/security.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/security.adoc @@ -12,7 +12,3 @@ reference documentation, including: * {docs-spring-security}/reactive/test/index.html[WebFlux Testing Support] * {docs-spring-security}/features/exploits/csrf.html#csrf-protection[CSRF protection] * {docs-spring-security}/features/exploits/headers.html[Security Response Headers] - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webflux/uri-building.adoc b/framework-docs/modules/ROOT/pages/web/webflux/uri-building.adoc index 8ca87a11f346..1ab092df7407 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/uri-building.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/uri-building.adoc @@ -7,5 +7,3 @@ This section describes various options available in the Spring Framework to prepare URIs. include::partial$web/web-uris.adoc[leveloffset=+1] - - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc index 1f73f66f3ecd..03b8950d7e40 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc @@ -4,8 +4,6 @@ This section describes options for client-side access to REST endpoints. - - [[webmvc-restclient]] == `RestClient` @@ -14,33 +12,30 @@ This section describes options for client-side access to REST endpoints. See xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] for more details. - - [[webmvc-webclient]] == `WebClient` -`WebClient` is a reactive client to perform HTTP requests with a fluent API. - -See xref:web/webflux-webclient.adoc[WebClient] for more details. - +`WebClient` is a reactive client for making HTTP requests with a fluent API. +See xref:web/webflux-webclient.adoc[`WebClient`] for more details. [[webmvc-resttemplate]] == `RestTemplate` -`RestTemplate` is a synchronous client to perform HTTP requests. It is the original +`RestTemplate` is a synchronous client for making HTTP requests. It is the original Spring REST client and exposes a simple, template-method API over underlying HTTP client libraries. -See xref:integration/rest-clients.adoc[REST Endpoints] for details. +See xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] for details. + -[[webmvc-http-interface]] -== HTTP Interface +[[webmvc-http-service-client]] +== HTTP Service Client -The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP +The Spring Framework lets you define an HTTP service as a Java interface with HTTP exchange methods. You can then generate a proxy that implements this interface and performs the exchanges. This helps to simplify HTTP remote access and provides additional -flexibility for to choose an API style such as synchronous or reactive. +flexibility for choosing an API style such as synchronous or reactive. -See xref:integration/rest-clients.adoc#rest-http-interface[REST Endpoints] for details. +See xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service Client] for details. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc index a8e7bb148b64..1e6f1be29df7 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc @@ -1,13 +1,12 @@ [[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 describes how to do so. - - [[mvc-cors-intro]] == Introduction [.small]#xref:web/webflux-cors.adoc#webflux-cors-intro[See equivalent in the Reactive stack]# @@ -23,8 +22,6 @@ what kind of cross-domain requests are authorized, rather than using less secure powerful workarounds based on IFRAME or JSONP. - - [[mvc-cors-credentialed-requests]] == Credentialed Requests [.small]#xref:web/webflux-cors.adoc#webflux-cors-credentialed-requests[See equivalent in the Reactive stack]# @@ -52,8 +49,6 @@ WARNING: While such wildcard configuration can be handy, it is recommended when a finite set of values instead to provide a higher level of security. - - [[mvc-cors-processing]] == Processing [.small]#xref:web/webflux-cors.adoc#webflux-cors-processing[See equivalent in the Reactive stack]# @@ -71,12 +66,12 @@ required CORS response headers set. In order to enable cross-origin requests (that is, the `Origin` header is present and differs from the host of the request), you need to have some explicitly declared CORS -configuration. If no matching CORS configuration is found, preflight requests are -rejected. No CORS headers are added to the responses of simple and actual CORS requests -and, consequently, browsers reject them. +configuration. If no matching CORS configuration is found, no CORS headers are added to +the responses to preflight, simple and actual CORS requests and, consequently, browsers +reject them. Each `HandlerMapping` can be -{spring-framework-api}/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 +83,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 -{spring-framework-api}/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] @@ -102,8 +97,6 @@ To learn more from the source or make advanced customizations, check the code be ==== - - [[mvc-cors-controller]] == `@CrossOrigin` [.small]#xref:web/webflux-cors.adoc#webflux-cors-controller[See equivalent in the Reactive stack]# @@ -116,7 +109,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 +130,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/account") @@ -178,7 +171,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 +192,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 @@ -215,6 +208,7 @@ Kotlin:: fun remove(@PathVariable id: Long) { // ... } + } ---- ====== @@ -225,7 +219,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 +241,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @CrossOrigin(maxAge = 3600) @RestController @@ -269,8 +263,6 @@ Kotlin:: ====== - - [[mvc-cors-global]] == Global Configuration [.small]#xref:web/webflux-cors.adoc#webflux-cors-global[See equivalent in the Reactive stack]# @@ -295,90 +287,9 @@ the `allowOriginPatterns` property may be used to match to a dynamic set of orig `maxAge` is set to 30 minutes. +You can enable CORS in the Spring MVC configuration as the following example shows: - -[[mvc-cors-global-java]] -=== Java Configuration -[.small]#xref:web/webflux-cors.adoc#webflux-cors-global[See equivalent in the Reactive stack]# - -To enable CORS in the MVC Java config, you can use the `CorsRegistry` callback, -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 addCorsMappings(CorsRegistry registry) { - - registry.addMapping("/api/**") - .allowedOrigins("https://domain2.com") - .allowedMethods("PUT", "DELETE") - .allowedHeaders("header1", "header2", "header3") - .exposedHeaders("header1", "header2") - .allowCredentials(true).maxAge(3600); - - // Add more mappings... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addCorsMappings(registry: CorsRegistry) { - - registry.addMapping("/api/**") - .allowedOrigins("https://domain2.com") - .allowedMethods("PUT", "DELETE") - .allowedHeaders("header1", "header2", "header3") - .exposedHeaders("header1", "header2") - .allowCredentials(true).maxAge(3600) - - // Add more mappings... - } - } ----- -====== - - - -[[mvc-cors-global-xml]] -=== XML Configuration - -To enable CORS in the XML namespace, you can use the `` element, -as the following example shows: - -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - - ----- - - - +include-code::./WebConfiguration[tag=snippet,indent=0] [[mvc-cors-filter]] == CORS Filter @@ -398,7 +309,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 +329,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 23f5e7045b05..2217b9f0f29f 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. @@ -8,15 +9,13 @@ It is an alternative to the annotation-based programming model but otherwise run the same xref:web/webmvc/mvc-servlet.adoc[DispatcherServlet]. - - [[webmvc-fn-overview]] == Overview [.small]#xref:web/webflux-functional.adoc#webflux-fn-overview[See equivalent in the Reactive stack]# In WebMvc.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes `ServerRequest` and returns a `ServerResponse`. -Both the request and the response object have immutable contracts that offer JDK 8-friendly +Both the request and the response object have immutable contracts that offer convenient access to the HTTP request and response. `HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the annotation-based programming model. @@ -34,7 +33,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 +70,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 @@ -109,19 +108,17 @@ Kotlin:: If you register the `RouterFunction` as a bean, for instance by exposing it in a -`@Configuration` class, it will be auto-detected by the servlet, as explained in xref:web/webmvc-functional.adoc#webmvc-fn-running[Running a Server]. - - +`@Configuration` class, it will be auto-detected by the servlet, as explained in +xref:web/webmvc-functional.adoc#webmvc-fn-running[Running a Server]. [[webmvc-fn-handler-functions]] == HandlerFunction [.small]#xref:web/webflux-functional.adoc#webflux-fn-handler-functions[See equivalent in the Reactive stack]# -`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly +`ServerRequest` and `ServerResponse` are immutable interfaces that offer convenient access to the HTTP request and response, including headers, body, method, and status code. - [[webmvc-fn-request]] === ServerRequest @@ -134,14 +131,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 +152,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,19 +171,39 @@ 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() ---- ====== +The following shows how to bind request parameters, URI variables, or headers via `DataBinder`, +and also shows how to customize the `DataBinder`: + +[tabs] +====== +Java:: ++ +[source,java] +---- +Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name")); +---- + +Kotlin:: ++ +[source,kotlin] +---- +val pet = request.bind(Pet::class.java, {dataBinder -> dataBinder.setAllowedFields("name")}) +---- +====== + [[webmvc-fn-response]] === ServerResponse @@ -200,7 +217,7 @@ content: ====== Java:: + -[source,java,role="primary"] +[source,java] ---- Person person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person); @@ -208,7 +225,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 +238,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 +246,7 @@ ServerResponse.created(location).build(); Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin] ---- val location: URI = ... ServerResponse.created(location).build() @@ -243,7 +260,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 +268,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 +284,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 +292,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 +300,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 +326,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 -> @@ -334,8 +351,6 @@ Kotlin:: ---- ====== - - [[webmvc-fn-handler-classes]] === Handler Classes @@ -346,7 +361,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 +369,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 +388,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 +434,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) { @@ -451,7 +466,6 @@ found. If it is not found, we return a 404 Not Found response. ====== -- - [[webmvc-fn-handler-validation]] === Validation @@ -463,7 +477,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 +507,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) { @@ -527,7 +541,6 @@ a global `Validator` instance based on `LocalValidatorFactoryBean`. See xref:core/validation/beanvalidation.adoc[Spring Validation]. - [[webmvc-fn-router-functions]] == `RouterFunction` [.small]#xref:web/webflux-functional.adoc#webflux-fn-router-functions[See equivalent in the Reactive stack]# @@ -542,28 +555,28 @@ to create a router. Generally, it is recommended to use the `route()` builder, as it provides convenient short-cuts for typical mapping scenarios without requiring hard-to-discover static imports. -For instance, the router function builder offers the method `GET(String, HandlerFunction)` to create a mapping for GET requests; and `POST(String, HandlerFunction)` for POSTs. +For instance, the router function builder offers the method `GET(String, HandlerFunction)` +to create a mapping for GET requests; and `POST(String, HandlerFunction)` for POSTs. Besides HTTP method-based mapping, the route builder offers a way to introduce additional predicates when mapping to requests. For each HTTP method there is an overloaded variant that takes a `RequestPredicate` as a parameter, through which additional constraints can be expressed. - [[webmvc-fn-predicates]] === Predicates You can write your own `RequestPredicate`, but the `RequestPredicates` utility class -offers commonly used implementations, based on the request path, HTTP method, content-type, -and so on. -The following example uses a request predicate to create a constraint based on the `Accept` -header: +offers built-in options for common needs for matching based on the HTTP method, request +path, headers, xref:#api-version[API version], and more. + +The following example uses an `Accept` header, request predicate: [tabs] ====== 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 +585,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 @@ -595,8 +608,6 @@ and `RequestPredicates.path(String)`. The example shown above also uses two request predicates, as the builder uses `RequestPredicates.GET` internally, and composes that with the `accept` predicate. - - [[webmvc-fn-routes]] === Routes @@ -624,7 +635,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 +662,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 @@ -676,7 +687,6 @@ Kotlin:: <4> `otherRoute` is a router function that is created elsewhere, and added to the route built. ====== - [[nested-routes]] === Nested Routes @@ -693,7 +703,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 +716,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 +740,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 +753,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 +770,51 @@ Kotlin:: ====== + +[[api-version]] +=== API Version + +Router functions support matching by API version. + +First, enable API versioning in the +xref:web/webmvc/mvc-config/api-version.adoc[MVC Config], and then you can use the +`version` xref:#webmvc-fn-predicates[predicate] as follows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + RouterFunction route = RouterFunctions.route() + .GET("/hello-world", version("1.2"), + request -> ServerResponse.ok().body("Hello World")).build(); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val route = router { + GET("/hello-world", version("1.2")) { + ServerResponse.ok().body("Hello World") + } + } +---- +====== + +The `version` predicate can be: + +- Fixed version ("1.2") -- matches the given version only +- Baseline version ("1.2+") -- matches the given version and above, up to the highest +xref:web/webmvc/mvc-config/api-version.adoc[supported version]. + +See xref:web/webmvc-versioning.adoc[API Versioning] for more details on underlying +infrastructure and support for API Versioning. + + + + [[webmvc-fn-serving-resources]] == Serving Resources @@ -778,11 +833,10 @@ for handling redirects in Single Page Applications. ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - ClassPathResource index = new ClassPathResource("static/index.html"); - List extensions = List.of("js", "css", "ico", "png", "jpg", "gif"); - RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate(); + ClassPathResource index = new ClassPathResource("static/index.html"); + RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate(); RouterFunction redirectToIndex = route() .resource(spaPredicate, index) .build(); @@ -790,13 +844,11 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val redirectToIndex = router { + val redirectToIndex = router { val index = ClassPathResource("static/index.html") - val extensions = listOf("js", "css", "ico", "png", "jpg", "gif") - val spaPredicate = !(path("/api/**") or path("/error") or - pathExtension(extensions::contains)) + val spaPredicate = !(path("/api/**") or path("/error")) resource(spaPredicate, index) } ---- @@ -811,18 +863,18 @@ It is also possible to route requests that match a given pattern to resources re ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- - Resource location = new FileSystemResource("public-resources/"); - RouterFunction resources = RouterFunctions.resources("/resources/**", location); + Resource location = new FileUrlResource("public-resources/"); + RouterFunction resources = RouterFunctions.resources("/resources/**", location); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val location = FileSystemResource("public-resources/") - val resources = router { resources("/resources/**", location) } + val location = FileUrlResource("public-resources/") + val resources = router { resources("/resources/**", location) } ---- ====== @@ -847,83 +899,9 @@ processing lifecycle and also (potentially) run side by side with annotated cont any are declared. It is also how functional endpoints are enabled by the Spring Boot Web starter. -The following example shows a WebFlux Java configuration: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableMvc - public class WebConfig implements WebMvcConfigurer { - - @Bean - public RouterFunction routerFunctionA() { - // ... - } - - @Bean - public RouterFunction routerFunctionB() { - // ... - } - - // ... - - @Override - public void configureMessageConverters(List> converters) { - // configure message conversion... - } - - @Override - public void addCorsMappings(CorsRegistry registry) { - // configure CORS... - } - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - // configure view resolution for HTML rendering... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableMvc - class WebConfig : WebMvcConfigurer { - - @Bean - fun routerFunctionA(): RouterFunction<*> { - // ... - } - - @Bean - fun routerFunctionB(): RouterFunction<*> { - // ... - } - - // ... - - override fun configureMessageConverters(converters: List>) { - // configure message conversion... - } - - override fun addCorsMappings(registry: CorsRegistry) { - // configure CORS... - } - - override fun configureViewResolvers(registry: ViewResolverRegistry) { - // configure view resolution for HTML rendering... - } - } ----- -====== - +The following example shows a related Spring MVC configuration: +include-code::./WebConfiguration[tag=snippet,indent=0] [[webmvc-fn-handler-filter-function]] @@ -941,7 +919,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 @@ -960,7 +938,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 @@ -998,7 +976,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 = ... @@ -1021,7 +999,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-versioning.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc new file mode 100644 index 000000000000..fdde8cd4c019 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc @@ -0,0 +1,108 @@ +[[mvc-versioning]] += API Versioning +:page-section-summary-toc: 1 + +[.small]#xref:web/webflux-versioning.adoc[See equivalent in the Reactive stack]# + +Spring MVC supports API versioning. This section provides an overview of the support +and underlying strategies. + +Please, see also related content in: + +- Configure xref:web/webmvc/mvc-config/api-version.adoc[API versioning] in the MVC Config +- xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[Map requests] +to annotated controller methods with an API version +- xref:web/webmvc-functional.adoc#api-version[Route requests] +to functional endpoints with an API version + +Client support for API versioning is available also in `RestClient`, `WebClient`, and +xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service] clients, as well as +for testing in MockMvc and `WebTestClient`. + + + + +[[mvc-versioning-strategy]] +== ApiVersionStrategy +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-strategy[See equivalent in the Reactive stack]# + +This is the central strategy for API versioning that holds all configured preferences +related to versioning. It does the following: + +- Resolves versions from the requests via xref:#mvc-versioning-resolver[ApiVersionResolver] +- Parses raw version values into `Comparable` with an xref:#mvc-versioning-parser[ApiVersionParser] +- xref:#mvc-versioning-validation[Validates] request versions +- Sends deprecation hints in the responses + +`ApiVersionStrategy` helps to map requests to `@RequestMapping` controller methods, +and is initialized by the MVC config. Typically, applications do not interact +directly with it. + + + + +[[mvc-versioning-resolver]] +== ApiVersionResolver +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-resolver[See equivalent in the Reactive stack]# + +This strategy resolves the API version from a request. The MVC config provides built-in +options to resolve from a header, query parameter, media type parameter, +or from the URL path. You can also use a custom `ApiVersionResolver`. + +The path resolver selects the version from a path segment specified by index, or +raises `InvalidApiVersionException`, and therefore never results in `null` (no version) +unless it is configured with a `Predicate` to determine if a path is versioned. + + + + +[[mvc-versioning-parser]] +== ApiVersionParser +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-parser[See equivalent in the Reactive stack]# + +This strategy helps to parse raw version values into `Comparable`, which helps to +compare, sort, and select versions. By default, the built-in `SemanticApiVersionParser` +parses a version into `major`, `minor`, and `patch` integer values. Minor and patch +values are set to 0 if not present. + + + + +[[mvc-versioning-validation]] +== Validation +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-validation[See equivalent in the Reactive stack]# + +If a request version is not supported, `InvalidApiVersionException` is raised resulting +in a 400 response. By default, the list of supported versions is initialized from declared +versions in annotated controller mappings, but you can turn that off through a flag in the +MVC config, and use only the versions configured explicitly in the config. + +By default, a version is required when API versioning is enabled, and +`MissingApiVersionException` is raised resulting in a 400 response if not present. +You can make it optional in which case the most recent version is used. +You can also specify a default version to use. + + + + +[[mvc-versioning-deprecation-handler]] +== ApiVersionDeprecationHandler +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-deprecation-handler[See equivalent in the Reactive stack]# + +This strategy can be configured to send hints and information about deprecated versions to +clients via response headers. The built-in `StandardApiVersionDeprecationHandler` +can set the "Deprecation" "Sunset" headers and "Link" headers as defined in +https://datatracker.ietf.org/doc/html/rfc9745[RFC 9745] and +https://datatracker.ietf.org/doc/html/rfc8594[RFC 8594]. You can also configure a custom +handler for different headers. + + + + +[[mvc-versioning-mapping]] +== Request Mapping +[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-mapping[See equivalent in the Reactive stack]# + +`ApiVersionStrategy` supports the mapping of requests to annotated controller methods. +See xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[API Version] +for more details. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view.adoc index e6af04b7fe26..b67ed2c96160 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view.adoc @@ -1,15 +1,16 @@ [[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 such, it is not recommended to use Spring MVC's template support in applications where the templates are editable by external sources, since this can have security implications. - 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..d1e50c6e53a7 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 @@ -4,11 +4,14 @@ Spring offers ways to return output other than HTML, including PDF and Excel spreadsheets. This section describes how to use those features. - +WARNING: As of Spring Framework 7.0, view classes in the `org.springframework.web.servlet.view.document` +package are deprecated. Instead, libraries can adapt this existing code to provide support with their own `*View` types. +As an alternative, applications can perform direct rendering in web handlers. [[mvc-view-document-intro]] == Introduction to Document Views + An HTML page is not always the best way for the user to view the model output, and Spring makes it simple to generate a PDF document or an Excel spreadsheet dynamically from the model data. The document is the view and is streamed from the @@ -24,7 +27,6 @@ instead of the outdated original iText 2.1.7, since OpenPDF is actively maintain fixes an important vulnerability for untrusted PDF content. - [[mvc-view-document-pdf]] == PDF Views @@ -36,7 +38,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 +55,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class PdfWordList : AbstractPdfView() { @@ -73,7 +75,6 @@ A controller can return such a view either from an external view definition (referencing it by name) or as a `View` instance from the handler method. - [[mvc-view-document-excel]] == Excel Views @@ -85,7 +86,3 @@ and `AbstractXlsxStreamingView`) that supersede the outdated `AbstractExcelView` The programming model is similar to `AbstractPdfView`, with `buildExcelDocument()` as the central template method and controllers being able to return such a view from an external definition (by name) or as a `View` instance from the handler method. - - - - 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 d12bf374fa40..78069fc588c2 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 @@ -1,6 +1,10 @@ [[mvc-view-feeds]] = RSS and Atom +WARNING: As of Spring Framework 7.0, view classes in the `org.springframework.web.servlet.view.feed` +package are deprecated. Instead, libraries can adapt this existing code to provide support with their own `*View` types. +As an alternative, applications can perform direct rendering in web handlers. + Both `AbstractAtomFeedView` and `AbstractRssFeedView` inherit from the `AbstractFeedView` base class and are used to provide Atom and RSS Feed views, respectively. They are based on https://rometools.github.io/rome/[ROME] project and are located in the @@ -14,7 +18,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 +38,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SampleContentAtomView : AbstractAtomFeedView() { @@ -57,7 +61,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 +81,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class SampleContentRssView : AbstractRssFeedView() { @@ -94,8 +98,6 @@ Kotlin:: ---- ====== - - The `buildFeedItems()` and `buildFeedEntries()` methods pass in the HTTP request, in case you need to access the Locale. The HTTP response is passed in only for the setting of cookies or other HTTP headers. The feed is automatically written to the response @@ -103,7 +105,3 @@ object after the method returns. For an example of creating an Atom view, see Alef Arendsen's Spring Team Blog {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..10a4842bf0a2 --- /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`. 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..8688a7bb484e 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 @@ -8,86 +8,13 @@ kind of text output from HTML to email and others. The Spring Framework has buil integration for using Spring MVC with FreeMarker templates. - [[mvc-view-freemarker-contextconfig]] == View Configuration [.small]#xref:web/webflux-view.adoc#webflux-view-freemarker-contextconfig[See equivalent in the Reactive stack]# The following example shows how to configure FreeMarker as a view technology: -[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.freeMarker(); - } - - // Configure FreeMarker... - - @Bean - public FreeMarkerConfigurer freeMarkerConfigurer() { - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setTemplateLoaderPath("/WEB-INF/freemarker"); - return configurer; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.freeMarker() - } - - // Configure FreeMarker... - - @Bean - fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { - setTemplateLoaderPath("/WEB-INF/freemarker") - } - } ----- -====== - -The following example shows how to configure the same in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - ----- - -Alternatively, you can also declare the `FreeMarkerConfigurer` bean for full control over all -properties, as the following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] Your templates need to be stored in the directory specified by the `FreeMarkerConfigurer` shown in the preceding example. Given the preceding configuration, if your controller @@ -95,7 +22,6 @@ returns a view name of `welcome`, the resolver looks for the `/WEB-INF/freemarker/welcome.ftl` template. - [[mvc-views-freemarker]] == FreeMarker Configuration [.small]#xref:web/webflux-view.adoc#webflux-views-freemarker[See equivalent in the Reactive stack]# @@ -106,27 +32,15 @@ properties on the `FreeMarkerConfigurer` bean. The `freemarkerSettings` property a `java.util.Properties` object, and the `freemarkerVariables` property requires a `java.util.Map`. The following example shows how to use a `FreeMarkerConfigurer`: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] See the FreeMarker documentation for details of settings and variables as they apply to 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 @@ -134,7 +48,6 @@ form-backing objects and show the results of failed validations from a `Validato web or business tier. Spring also has support for the same functionality in FreeMarker, with additional convenience macros for generating form input elements themselves. - [[mvc-view-bind-macros]] === The Bind Macros [.small]#xref:web/webflux-view.adoc#webflux-view-bind-macros[See equivalent in the Reactive stack]# @@ -149,7 +62,6 @@ you need to directly call from within your templates. If you wish to view the ma directly, the file is called `spring.ftl` and is in the `org.springframework.web.servlet.view.freemarker` package. - [[mvc-view-simple-binding]] === Simple Binding @@ -193,7 +105,6 @@ messages or values. You can set it to `true` or `false` as required. Additional handling macros simplify the use of HTML escaping, and you should use these macros wherever possible. They are explained in the next section. - [[mvc-views-form-macros]] === Input Macros @@ -271,7 +182,7 @@ The parameters to any of the above macros have consistent meanings: For strictly sorted maps, you can use a `SortedMap` (such as a `TreeMap`) with a suitable `Comparator` and, for arbitrary Maps that should return values in insertion order, use a `LinkedHashMap` or a `LinkedMap` from `commons-collections`. -* `separator`: Where multiple options are available as discreet elements (radio buttons +* `separator`: Where multiple options are available as discrete elements (radio buttons or checkboxes), the sequence of characters used to separate each one in the list (such as `
`). * `attributes`: An additional string of arbitrary tags or text to be included within @@ -374,7 +285,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 +301,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( @@ -414,7 +325,6 @@ user still sees the more user-friendly city names, as follows: New York ---- - [[mvc-views-form-macros-html-escaping]] === HTML Escaping @@ -451,7 +361,3 @@ In similar fashion, you can specify HTML escaping per field, as the following ex <#assign htmlEscape = false in spring> <#-- all future fields will be bound with HTML escaping off --> ---- - - - - 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..d02af84b256e 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 @@ -9,75 +9,12 @@ integration for using Spring MVC with Groovy Markup. NOTE: The Groovy Markup Template engine requires Groovy 2.3.1+. - [[mvc-view-groovymarkup-configuration]] == Configuration The following example shows how to configure the Groovy Markup Template Engine: -[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.groovy(); - } - - // Configure the Groovy Markup Template Engine... - - @Bean - public GroovyMarkupConfigurer groovyMarkupConfigurer() { - GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer(); - configurer.setResourceLoaderPath("/WEB-INF/"); - return configurer; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.groovy() - } - - // Configure the Groovy Markup Template Engine... - - @Bean - fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply { - resourceLoaderPath = "/WEB-INF/" - } - } ----- -====== - -The following example shows how to configure the same in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- - - +include-code::./WebConfiguration[tag=snippet,indent=0] [[mvc-view-groovymarkup-example]] == Example @@ -98,7 +35,3 @@ syntax. The following example shows a sample template for an HTML page: } } ---- - - - - 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 3b419811c325..dfd7c039ee83 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 @@ -6,12 +6,11 @@ Spring offers support for the Jackson JSON library. - [[mvc-view-json-mapping]] == Jackson-based JSON MVC Views [.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]# -The `MappingJackson2JsonView` uses the Jackson library's `ObjectMapper` to render the response +The `JacksonJsonView` uses the Jackson library's `JsonMapper` to render the response content as JSON. By default, the entire contents of the model map (with the exception of framework-specific classes) are encoded as JSON. For cases where the contents of the map need to be filtered, you can specify a specific set of model attributes to encode @@ -19,18 +18,17 @@ by using the `modelKeys` property. You can also use the `extractValueFromSingleK property to have the value in single-key models extracted and serialized directly rather than as a map of model attributes. -You can customize JSON mapping as needed by using Jackson's provided -annotations. When you need further control, you can inject a custom `ObjectMapper` -through the `ObjectMapper` property, for cases where you need to provide custom JSON -serializers and deserializers for specific types. - +You can customize JSON mapping as needed by using Jackson's provided annotations. When +you need further control, you can inject a custom `JsonMapper` through the `JsonMapper` +or `JsonMapper.Builder` constructor parameters, for cases where you need to provide +custom JSON serializers and deserializers for specific types. [[mvc-view-xml-mapping]] == Jackson-based XML Views [.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]# -`MappingJackson2XmlView` uses the +`JacksonXmlView` uses the {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 @@ -38,9 +36,5 @@ model contains a single entry, it is serialized automatically. 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. - - - - +created via `XmlMapper.Builder` 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 51ad8f57bf73..42348d86265a 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 @@ -4,7 +4,6 @@ The Spring Framework has a built-in integration for using Spring MVC with JSP and JSTL. - [[mvc-view-jsp-resolver]] == View Resolvers @@ -12,26 +11,21 @@ When developing with JSPs, you typically declare an `InternalResourceViewResolve `InternalResourceViewResolver` can be used for dispatching to any Servlet resource but in particular for JSPs. As a best practice, we strongly encourage placing your JSP files in -a directory under the `'WEB-INF'` directory so there can be no direct access by clients. +a directory under the `WEB-INF` directory so there can be no direct access by clients. -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - ----- +This is what is done by the configuration below which registers a JSP view resolver using +a default view name prefix of `"/WEB-INF/"` and a default suffix of `".jsp"`. +include-code::./WebConfiguration[tag=snippet,indent=0] +[NOTE] +You can specify custom prefix and suffix. [[mvc-view-jsp-jstl]] == JSPs versus JSTL When using the JSP Standard Tag Library (JSTL) you must use a special view class, the -`JstlView`, as JSTL needs some preparation before things such as the I18N features can -work. - +`JstlView`, as JSTL needs some preparation before things such as the I18N features can work. [[mvc-view-jsp-tags]] @@ -64,7 +58,6 @@ JSPs easier to develop, read, and maintain. We go through the form tags and look at an example of how each tag is used. We have included generated HTML snippets where certain tags require further commentary. - [[mvc-view-jsp-formtaglib-configuration]] === Configuration @@ -80,7 +73,6 @@ page: ---- where `form` is the tag name prefix you want to use for the tags from this library. - [[mvc-view-jsp-formtaglib-formtag]] === The Form Tag @@ -168,7 +160,6 @@ following example shows: ---- - [[mvc-view-jsp-formtaglib-inputtag]] === The `input` Tag @@ -176,7 +167,6 @@ This tag renders an HTML `input` element with the bound value and `type='text'` For an example of this tag, see xref:web/webmvc-view/mvc-jsp.adoc#mvc-view-jsp-formtaglib-formtag[The Form Tag]. You can also use HTML5-specific types, such as `email`, `tel`, `date`, and others. - [[mvc-view-jsp-formtaglib-checkboxtag]] === The `checkbox` Tag @@ -189,7 +179,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 +215,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class Preferences( var receiveNewsletter: Boolean, @@ -306,7 +296,6 @@ prefixed by an underscore (`_`) for each checkbox. By doing this, you are effect telling Spring that "`the checkbox was visible in the form, and I want my object to which the form data binds to reflect the state of the checkbox, no matter what.`" - [[mvc-view-jsp-formtaglib-checkboxestag]] === The `checkboxes` Tag @@ -341,8 +330,6 @@ the map entry key is used as the value, and the map entry's value is used as the label to be displayed. You can also use a custom object where you can provide the property names for the value by using `itemValue` and the label by using `itemLabel`. - - [[mvc-view-jsp-formtaglib-radiobuttontag]] === The `radiobutton` Tag @@ -362,7 +349,6 @@ but with different values, as the following example shows: ---- - [[mvc-view-jsp-formtaglib-radiobuttonstag]] === The `radiobuttons` Tag @@ -384,7 +370,6 @@ by using `itemValue` and the label by using `itemLabel`, as the following exampl ---- - [[mvc-view-jsp-formtaglib-passwordtag]] === The `password` Tag @@ -414,7 +399,6 @@ password value to be shown, you can set the value of the `showPassword` attribut ---- - [[mvc-view-jsp-formtaglib-selecttag]] === The `select` Tag @@ -448,7 +432,6 @@ as follows: ---- - [[mvc-view-jsp-formtaglib-optiontag]] === The `option` Tag @@ -489,7 +472,6 @@ as follows: ---- <1> Note the addition of a `selected` attribute. - [[mvc-view-jsp-formtaglib-optionstag]] === The `options` Tag @@ -540,7 +522,6 @@ values and the map values correspond to option labels. If `itemValue` or `itemLa happen to be specified as well, the item value property applies to the map key, and the item label property applies to the map value. - [[mvc-view-jsp-formtaglib-textareatag]] === The `textarea` Tag @@ -555,7 +536,6 @@ This tag renders an HTML `textarea` element. The following HTML shows typical ou ---- - [[mvc-view-jsp-formtaglib-hiddeninputtag]] === The `hidden` Tag @@ -576,7 +556,6 @@ If we choose to submit the `house` value as a hidden one, the HTML would be as f ---- - [[mvc-view-jsp-formtaglib-errorstag]] === The `errors` Tag @@ -592,7 +571,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 +588,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- class UserValidator : Validator { @@ -748,8 +727,6 @@ For a comprehensive reference on individual tags, browse the {spring-framework-api}/web/servlet/tags/form/package-summary.html#package.description[API reference] or see the tag library description. - - [[mvc-rest-method-conversion]] === HTTP Method Conversion @@ -801,7 +778,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 +789,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 { @@ -832,7 +809,3 @@ The form `input` tag supports entering a type attribute other than `text`. This intended to allow rendering new HTML5 specific input types, such as `email`, `date`, `range`, and others. Note that entering `type='text'` is not required, since `text` is the default type. - - - - 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 adc87d900352..01657f79b558 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 @@ -11,120 +11,49 @@ templating libraries on different script engines: [%header] |=== |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://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby] |https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython] -|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin] |=== TIP: The basic rule for integrating any other script engine is that it must implement the `ScriptEngine` and `Invocable` interfaces. - [[mvc-view-script-dependencies]] == Requirements [.small]#xref:web/webflux-view.adoc#webflux-view-script-dependencies[See equivalent in the Reactive stack]# You need to have the script engine on your classpath, the details of which vary by script engine: -* The https://openjdk.java.net/projects/nashorn/[Nashorn] JavaScript engine is provided with -Java 8+. Using the latest update release available is highly recommended. * https://www.jruby.org[JRuby] should be added as a dependency for Ruby support. * https://www.jython.org[Jython] should be added as a dependency for Python support. -* `org.jetbrains.kotlin:kotlin-script-util` dependency and a `META-INF/services/javax.script.ScriptEngineFactory` - file containing a `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory` - line should be added for Kotlin script support. See - https://github.com/sdeleuze/kotlin-script-templating[this example] for more details. - -You need to have the script templating library. One way to do that for JavaScript is -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. -The following example uses Mustache templates and the Nashorn JavaScript engine: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { +The following example uses the Jython Python engine: - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.scriptTemplate(); - } - - @Bean - public ScriptTemplateConfigurer configurer() { - ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); - configurer.setEngineName("nashorn"); - configurer.setScripts("mustache.js"); - configurer.setRenderObject("Mustache"); - configurer.setRenderFunction("render"); - return configurer; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.scriptTemplate() - } +include-code::./WebConfiguration[tag=snippet,indent=0] - @Bean - fun configurer() = ScriptTemplateConfigurer().apply { - engineName = "nashorn" - setScripts("mustache.js") - renderObject = "Mustache" - renderFunction = "render" - } - } ----- -====== - -The following example shows the same arrangement in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - +The render function is called with the following parameters: - - - ----- +* `String template`: The template content +* `Map model`: The view model +* `RenderingContext renderingContext`: The +{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 -The controller would look no different for the Java and XML configurations, as the following example shows: +The controller is used to populate the model attributes and specify the view name, as the following example shows: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @Controller public class SampleController { @@ -140,7 +69,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller class SampleController { @@ -155,120 +84,7 @@ Kotlin:: ---- ====== -The following example shows the Mustache template: - -[source,html,indent=0,subs="verbatim,quotes"] ----- - - - {{title}} - - -

{{body}}

- - ----- - -The render function is called with the following parameters: - -* `String template`: The template content -* `Map model`: The view model -* `RenderingContext renderingContext`: The - {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) - -`Mustache.render()` is natively compatible with this signature, so you can call it directly. - -If your templating technology requires some customization, you can provide a script that -implements a custom render function. For example, https://handlebarsjs.com[Handlerbars] -needs to compile templates before using them and requires a -https://en.wikipedia.org/wiki/Polyfill[polyfill] to emulate some -browser facilities that are not available in the server-side script engine. - -The following example shows how to do so: - -[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.scriptTemplate(); - } - - @Bean - public ScriptTemplateConfigurer configurer() { - ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); - configurer.setEngineName("nashorn"); - configurer.setScripts("polyfill.js", "handlebars.js", "render.js"); - configurer.setRenderFunction("render"); - configurer.setSharedEngine(false); - return configurer; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.scriptTemplate() - } - - @Bean - fun configurer() = ScriptTemplateConfigurer().apply { - engineName = "nashorn" - setScripts("polyfill.js", "handlebars.js", "render.js") - renderFunction = "render" - isSharedEngine = false - } - } ----- -====== - -NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe -script engines with templating libraries not designed for concurrency, such as Handlebars or -React running on Nashorn. In that case, Java SE 8 update 60 is required, due to -https://bugs.openjdk.java.net/browse/JDK-8076099[this bug], but it is generally -recommended to use a recent Java SE patch release in any case. - -`polyfill.js` defines only the `window` object needed by Handlebars to run properly, as follows: - -[source,javascript,indent=0,subs="verbatim,quotes"] ----- - var window = {}; ----- - -This basic `render.js` implementation compiles the template before using it. A production-ready -implementation should also store any reused cached templates or pre-compiled templates. -You can do so on the script side (and handle any customization you need -- managing -template engine configuration, for example). The following example shows how to do so: - -[source,javascript,indent=0,subs="verbatim,quotes"] ----- - function render(template, model) { - var compiledTemplate = Handlebars.compile(template); - return compiledTemplate(model); - } ----- - Check out the Spring Framework unit tests, {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-thymeleaf.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-thymeleaf.adoc index 48cf0c6d5ab6..749530ee1798 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-thymeleaf.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-thymeleaf.adoc @@ -16,7 +16,3 @@ The Thymeleaf integration with Spring MVC is managed by the Thymeleaf project. The configuration involves a few bean declarations, such as `ServletContextTemplateResolver`, `SpringTemplateEngine`, and `ThymeleafViewResolver`. See https://www.thymeleaf.org/documentation.html[Thymeleaf+Spring] for more details. - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-xml-marshalling.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-xml-marshalling.adoc index 74f65108f212..db74ad83e4b3 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-xml-marshalling.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-xml-marshalling.adoc @@ -8,7 +8,3 @@ marshalled by using a `MarshallingView` instance's `modelKey` bean property. Alt the view iterates over all model properties and marshals the first type that is supported by the `Marshaller`. For more information on the functionality in the `org.springframework.oxm` package, see xref:data-access/oxm.adoc[Marshalling XML using O/X Mappers]. - - - - 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..8e8358335e84 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 @@ -9,10 +9,9 @@ XSLT in a Spring Web MVC application. This example is a trivial Spring application that creates a list of words in the `Controller` and adds them to the model map. The map is returned, along with the view -name of our XSLT view. See xref:web/webmvc/mvc-controller.adoc[Annotated Controllers] for details of Spring Web MVC's -`Controller` interface. The XSLT controller turns the list of words into a simple XML -document ready for transformation. - +name of our XSLT view. See xref:web/webmvc/mvc-controller.adoc[Annotated Controllers] +for details of Spring Web MVC's `Controller` interface. The XSLT controller turns the +list of words into a simple XML document ready for transformation. [[mvc-view-xslt-beandefs]] @@ -22,45 +21,7 @@ Configuration is standard for a simple Spring web application: The MVC configura has to define an `XsltViewResolver` bean and regular MVC annotation configuration. The following example shows how to do so: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @EnableWebMvc - @ComponentScan - @Configuration - public class WebConfig implements WebMvcConfigurer { - - @Bean - public XsltViewResolver xsltViewResolver() { - XsltViewResolver viewResolver = new XsltViewResolver(); - viewResolver.setPrefix("/WEB-INF/xsl/"); - viewResolver.setSuffix(".xslt"); - return viewResolver; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @EnableWebMvc - @ComponentScan - @Configuration - class WebConfig : WebMvcConfigurer { - - @Bean - fun xsltViewResolver() = XsltViewResolver().apply { - setPrefix("/WEB-INF/xsl/") - setSuffix(".xslt") - } - } ----- -====== - +include-code::./WebConfiguration[tag=snippet,indent=0] [[mvc-view-xslt-controllercode]] == Controller @@ -74,7 +35,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 +61,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 +98,6 @@ too great a part in the structure of your model data, which is a danger when usi to manage the DOMification process. - [[mvc-view-xslt-transforming]] == Transformation diff --git a/framework-docs/modules/ROOT/pages/web/webmvc.adoc b/framework-docs/modules/ROOT/pages/web/webmvc.adoc index 8eaac910662d..36b77455467c 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc.adoc @@ -19,4 +19,3 @@ 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 {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 823171a36968..11bd6848a925 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc @@ -3,13 +3,30 @@ [.small]#xref:web/webflux/reactive-spring.adoc#webflux-filters[See equivalent in the Reactive stack]# -The `spring-web` module provides some useful filters: +In the Servlet API, you can add a `jakarta.servlet.Filter` to apply interception-style logic +before and after the rest of the processing chain of filters and the target `Servlet`. + +The `spring-web` module has a number of built-in `Filter` implementations: * xref:web/webmvc/filters.adoc#filters-http-put[Form Data] * 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] + +There are also base class implementations for use in Spring applications: + +* `GenericFilterBean` -- base class for a `Filter` configured as a Spring bean; +integrates with the Spring `ApplicationContext` lifecycle. +* `OncePerRequestFilter` -- extension of `GenericFilterBean` that supports a single +invocation at the start of a request, i.e. during the `REQUEST` dispatch phase, and +ignoring further handling via `FORWARD` dispatches. The filter also provides control +over whether the `Filter` gets involved in `ASYNC` and `ERROR` dispatches. +Servlet filters can be configured in `web.xml` or via Servlet annotations. +In a Spring Boot application, you can +{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare Filter's as beans] +and Boot will have them configured. [[filters-http-put]] @@ -25,34 +42,36 @@ the body of the request, and wrap the `ServletRequest` to make the form data available through the `ServletRequest.getParameter{asterisk}()` family of methods. - -[[forwarded-headers]] +[[filters-forwarded-headers]] == Forwarded Headers [.small]#xref:web/webflux/reactive-spring.adoc#webflux-forwarded-headers[See equivalent in the Reactive stack]# include::partial$web/forwarded-headers.adoc[] - - [[filters-forwarded-headers-non-forwardedheaderfilter]] === ForwardedHeaderFilter -`ForwardedHeaderFilter` is a Servlet filter that modifies the request in order to -a) change the host, port, and scheme based on `Forwarded` headers, and b) to remove those -headers to eliminate further impact. The filter relies on wrapping the request, and -therefore it must be ordered ahead of other filters, such as `RequestContextFilter`, that -should work with the modified and not the original request. - +`ForwardedHeaderFilter` is a Servlet filter that modifies the request to match information +from the standard `"Forwarded"` or `"X-Forwarded"` headers, and also removes those headers +to eliminate further impact. The filter wraps the request and must be ordered ahead +of other filters such as `RequestContextFilter` in order for all downstream +handlers to see the modified request. [[filters-forwarded-headers-security]] === Security Considerations -There are security considerations for forwarded headers since an application cannot know -if the headers were added by a proxy, as intended, or by a malicious client. This is why -a proxy at the boundary of trust should be configured to remove untrusted `Forwarded` -headers that come from the outside. You can also configure the `ForwardedHeaderFilter` -with `removeOnly=true`, in which case it removes but does not use the headers. +Forwarded headers are intended to be set by trusted proxies and never allowed in from the +outside. A proxy at the edge of trust must remove forwarded headers including both the +standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use, +to protect applications which may check both. + +When creating `ForwardedHeaderFilter` you need to specify whether it should use the +standard `"Forwarded"` or `"X-Forwarded"` headers. If needed `"X-Forwarded-Prefix"` +must be enabled separately through a property on the filter. + +`ForwardedHeaderFilter` can be configured in `removeOnly` mode, in which case it removes +forwarded headers from the request without using them. @@ -68,7 +87,6 @@ types. However if registering the filter via `web.xml` or in Spring Boot via a `DispatcherType.ERROR` in addition to `DispatcherType.REQUEST`. - [[filters-shallow-etag]] == Shallow ETag @@ -96,7 +114,6 @@ the filter via `web.xml` or in Spring Boot via a `FilterRegistrationBean` be sur `DispatcherType.ASYNC`. - [[filters-cors]] == CORS [.small]#xref:web/webflux/reactive-spring.adoc#webflux-filters-cors[See equivalent in the Reactive stack]# @@ -108,5 +125,30 @@ controllers. However, when used with Spring Security, we advise relying on the b See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors.adoc#mvc-cors-filter[CORS Filter] for more details. +[[filters.url-handler]] +== URL Handler +[.small]#xref:web/webflux/reactive-spring.adoc#filters.url-handler[See equivalent in the Reactive 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")`. + +Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash. +This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings. +The filter can remove the trailing slash in one of a couple of ways: + +* respond with an HTTP redirect status that sends clients to the same path without a trailing slash. +* wrap the request to remove the trailing slash. + +NOTE: Historically Spring MVC supported trailing slash matching of URL paths. +This capability was deprecated in 6.0 for security reasons and removed in 7.0 with +`UrlHandlerFilter` providing a safer alternative. + +Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application: + +include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0] +Keep in mind the following: +- the root path `"/"` is excluded from trailing slash handling. +- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will +not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead. 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..c9e48ff2c82b --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc @@ -0,0 +1,90 @@ +[[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 URL encoded forms. +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`. +`Map` is also supported, but multiple values under the same key will be ignored. + +| `MultipartHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write multipart messages. +`MultiValueMap` can be written to multipart messages, converting each part independently using +the configured message converters. Multipart messages can be read into `MultiValueMap`, each value +being a `Part` or one of its subtypes (`FormFieldPart` and `FilePart`). +By default, `multipart/form-data` is supported. Additional multipart subtypes can be supported for writing form data. + +| `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`. + +| `JacksonJsonHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `JsonMapper`. +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 `JsonMapper` through the `JsonMapper` or `JsonMapper.Builder` constructor parameters. +By default, this converter supports `application/json`. This requires the `tools.jackson.core:jackson-databind` dependency. + +| `JacksonXmlHttpMessageConverter` +| 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 `JsonMapper` or `JsonMapper.Builder` constructor parameters. +By default, this converter supports `application/xml`. This requires the `tools.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. + +| `JacksonCborHttpMessageConverter` +| `tools.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 cab7cdae7f26..137da77f9602 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc @@ -4,16 +4,19 @@ Spring MVC has an extensive integration with Servlet asynchronous request xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-processing[processing]: -* xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-deferredresult[`DeferredResult`] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[`Callable`] -return values in controller methods provide basic support for a single asynchronous -return value. +* xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-deferredresult[`DeferredResult`], +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[`Callable`], and +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-webasynctask[`WebAsyncTask`] return values +in controller methods provide support for a single asynchronous return value. * Controllers can xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[stream] multiple values, including -xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-sse[SSE] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-output-stream[raw data]. +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-sse[SSE] and +xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-output-stream[raw data]. * Controllers can use reactive clients and return xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive types] for response handling. For an overview of how this differs from Spring WebFlux, see the xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-vs-webflux[Async Spring MVC compared to WebFlux] section below. + [[mvc-ann-async-deferredresult]] == `DeferredResult` @@ -25,7 +28,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 +44,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/quotes") @ResponseBody @@ -60,7 +63,6 @@ The controller can produce the return value asynchronously, from a different thr example, in response to an external event (JMS message), a scheduled task, or other event. - [[mvc-ann-async-callable]] == `Callable` @@ -71,7 +73,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 +83,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 { @@ -95,6 +97,43 @@ The return value can then be obtained by running the given task through the xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] `AsyncTaskExecutor`. +[[mvc-ann-async-webasynctask]] +== `WebAsyncTask` + +`WebAsyncTask` is comparable to using xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[Callable] +but allows customizing additional settings such a request timeout value, and the +`AsyncTaskExecutor` to execute the `java.util.concurrent.Callable` with instead +of the defaults set up globally for Spring MVC. Below is an example of using `WebAsyncTask`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @GetMapping("/callable") + WebAsyncTask handle() { + return new WebAsyncTask(20000L,()->{ + Thread.sleep(10000); //simulate long-running task + return "asynchronous request completed"; + }); + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- +@GetMapping("/callable") +fun handle(): WebAsyncTask { + return WebAsyncTask(20000L) { + Thread.sleep(10000) // simulate long-running task + "asynchronous request completed" + } +} +---- +====== + [[mvc-ann-async-processing]] == Processing @@ -140,7 +179,6 @@ For further background and context, you can also read {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. - [[mvc-ann-async-exceptions]] === Exception Handling @@ -154,7 +192,6 @@ The exception then goes through the regular exception handling mechanism (for ex When you use `Callable`, similar processing logic occurs, the main difference being that the result is returned from the `Callable` or an exception is raised by it. - [[mvc-ann-async-interception]] === Interception @@ -173,7 +210,6 @@ See the {spring-framework-api}/web/context/request/async/DeferredResult.html[jav for more details. `Callable` can be substituted for `WebAsyncTask` that exposes additional methods for timeout and completion callbacks. - [[mvc-ann-async-vs-webflux]] === Async Spring MVC compared to WebFlux @@ -214,7 +250,6 @@ You can use `DeferredResult` and `Callable` for a single asynchronous return val What if you want to produce multiple asynchronous values and have those written to the response? This section describes how to do so. - [[mvc-ann-async-objects]] === Objects @@ -227,7 +262,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 +283,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 { @@ -276,12 +311,11 @@ or `emitter.completeWithError`. Instead, the servlet container automatically ini This call, in turn, performs one final `ASYNC` dispatch to the application, during which Spring MVC invokes the configured exception resolvers and completes the request. - [[mvc-ann-async-sse]] === 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 +323,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 +344,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 { @@ -336,7 +370,6 @@ a wide range of browsers. See also xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-objects[previous section] for notes on exception handling. - [[mvc-ann-async-output-stream]] === Raw Data @@ -348,7 +381,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 +396,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/download") fun handle() = StreamingResponseBody { @@ -376,13 +409,12 @@ You can use `StreamingResponseBody` as the body in a `ResponseEntity` to customize the status and headers of the response. - [[mvc-ann-async-reactive-types]] == Reactive Types [.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs-streaming[See equivalent in the Reactive stack]# Spring MVC supports use of reactive client libraries in a controller (also read -xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries] in the WebFlux section). +xref:web/webflux-reactive-libraries.adoc[Reactive Libraries] in the WebFlux section). This includes the `WebClient` from `spring-webflux` and others, such as Spring Data reactive data repositories. In such scenarios, it is convenient to be able to return reactive types from the controller method. @@ -390,9 +422,9 @@ reactive types from the controller method. Reactive return values are handled as follows: * A single-value promise is adapted to, similar to using `DeferredResult`. Examples -include `Mono` (Reactor) or `Single` (RxJava). -* A multi-value stream with a streaming media type (such as `application/x-ndjson` -or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or +include `CompletionStage` (JDK), `Mono` (Reactor), and `Single` (RxJava). +* A multi-value stream with a streaming media type (such as `application/jsonl`, +`application/x-ndjson` or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or `SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava). Applications can also return `Flux` or `Observable`. * A multi-value stream with any other media type (such as `application/json`) is adapted @@ -409,8 +441,6 @@ xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[config from `WebClient`. - - [[mvc-ann-async-context-propagation]] == Context Propagation @@ -452,10 +482,8 @@ The following `ThreadLocalAccessor` implementations are provided out of the box: The above are not registered automatically. You need to register them via `ContextRegistry.getInstance()` on startup. -For more details, see the -https://micrometer.io/docs/contextPropagation[documentation] of the Micrometer Context -Propagation library. - +For more details, see the {micrometer-context-propagation-docs}/[documentation] of the +Micrometer Context Propagation library. [[mvc-ann-async-disconnects]] @@ -474,14 +502,12 @@ xref:web/websocket/stomp.adoc[STOMP over WebSocket] or WebSocket with xref:web/w that have a built-in heartbeat mechanism. - [[mvc-ann-async-configuration]] == Configuration The asynchronous request processing feature must be enabled at the Servlet container level. The MVC configuration also exposes several options for asynchronous requests. - [[mvc-ann-async-configuration-servlet3]] === Servlet Container @@ -496,7 +522,6 @@ In `web.xml` configuration, you can add `true `DispatcherServlet` and to `Filter` declarations and add `ASYNC` to filter mappings. - [[mvc-ann-async-configuration-spring-mvc]] === Spring MVC @@ -518,4 +543,3 @@ The one used by default is not suitable for production under load. Note that you can also set the default timeout value on a `DeferredResult`, a `ResponseBodyEmitter`, and an `SseEmitter`. For a `Callable`, you can use `WebAsyncTask` to provide a timeout value. - 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 adc8da9c8e0b..a035248477ad 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 @@ -22,7 +22,6 @@ xref:web/webmvc/mvc-controller/ann-advice.adoc[@ControllerAdvice] that handles a and any `ErrorResponseException`, and renders an error response with a body. - [[mvc-ann-rest-exceptions-render]] == Render [.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-render[See equivalent in the Reactive stack]# @@ -33,11 +32,11 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a - The `status` property of `ProblemDetail` determines the HTTP status. - The `instance` property of `ProblemDetail` is set from the current URL path, if not already set. -- For content negotiation, the Jackson `HttpMessageConverter` prefers -"application/problem+json" over "application/json" when rendering a `ProblemDetail`, -and also falls back on it if no compatible media type is found. +- The Jackson JSON and XML codecs use "application/problem+json" or +"application/problem+xml" respectively as the producible media types for `ProblemDetail` +to ensure they are favored for content negotiation. -To enable RFC 9457 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 @@ -46,8 +45,7 @@ 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 7807 response and take some action. - +any RFC 9457 response and take some action. [[mvc-ann-rest-exceptions-non-standard]] @@ -64,10 +62,16 @@ 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. +TIP: In Spring Boot, the `spring.mvc.problemdetails.enabled` property autoconfigures +a `ResponseEntityExceptionHandler` that handles built-in exceptions with problem details. +In that case, you may prefer to create another `@ControllerAdvice` instead of extending +`ResponseEntityExceptionHandler` if you want to take over the handling of a specific +built-in exception. You'll need to ensure your handler is ordered ahead of the one +configured by Spring Boot whose order is 0. [[mvc-ann-rest-exceptions-i18n]] @@ -174,11 +178,11 @@ Message codes and arguments for each error are also resolved via `MessageSource` | `NoResourceFoundException` | (default) -| +| `+{0}+` the request path (or portion of) used to find a resource | `TypeMismatchException` | (default) -| `+{0}+` property name, `+{1}+` property value +| `+{0}+` property name, `+{1}+` property value, `+{2}+` simple name of required type | `UnsatisfiedServletRequestParameterException` | (default) @@ -187,7 +191,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 based on a list of +`MethodArgumentNotValidException` 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 @@ -195,7 +199,6 @@ xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method for more details. - [[mvc-ann-rest-exceptions-client]] == Client Handling [.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-client[See equivalent in the Reactive stack]# @@ -204,6 +207,3 @@ A client application can catch `WebClientResponseException`, when using the `Web or `RestClientResponseException` when using the `RestTemplate`, and use their `getResponseBodyAs` methods to decode the error response body to any target type such as `ProblemDetail`, or a subclass of `ProblemDetail`. - - - 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 2ae6c92f6c96..fef2df4f2407 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-caching.adoc @@ -14,7 +14,6 @@ the `Last-Modified` header. This section describes the HTTP caching-related options that are available in Spring Web MVC. - [[mvc-caching-cachecontrol]] == `CacheControl` [.small]#xref:web/webflux/caching.adoc#webflux-caching-cachecontrol[See equivalent in the Reactive stack]# @@ -36,7 +35,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 +51,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) @@ -76,7 +75,6 @@ works as follows: `'Cache-Control: max-age=n'` directive. - [[mvc-caching-etag-lastmodified]] == Controllers [.small]#xref:web/webflux/caching.adoc#webflux-caching-etag-lastmodified[See equivalent in the Reactive stack]# @@ -91,7 +89,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 +107,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 +137,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 +158,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? { @@ -181,14 +179,12 @@ Kotlin:: ====== -- - There are three variants for checking conditional requests against `eTag` values, `lastModified` values, or both. For conditional `GET` and `HEAD` requests, you can set the response to 304 (NOT_MODIFIED). For conditional `POST`, `PUT`, and `DELETE`, you can instead set the response to 412 (PRECONDITION_FAILED), to prevent concurrent modification. - [[mvc-caching-static-resources]] == Static Resources [.small]#xref:web/webflux/caching.adoc#webflux-caching-static-resources[See equivalent in the Reactive stack]# @@ -197,10 +193,8 @@ You should serve static resources with a `Cache-Control` and conditional respons for optimal performance. See the section on configuring xref:web/webmvc/mvc-config/static-resources.adoc[Static Resources]. - [[mvc-httpcaching-shallowetag]] == `ETag` Filter You can use the `ShallowEtagHeaderFilter` to add "`shallow`" `eTag` values that are computed from the response content and, thus, save bandwidth but not CPU time. See xref:web/webmvc/filters.adoc#filters-shallow-etag[Shallow ETag]. - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config.adoc index a29ad9a6e2bd..e6fed64b2790 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config.adoc @@ -13,6 +13,3 @@ see xref:web/webmvc/mvc-config/advanced-java.adoc[Advanced Java Config] and xref You do not need to understand the underlying beans created by the MVC Java configuration and the MVC namespace. If you want to learn more, see xref:web/webmvc/mvc-servlet/special-bean-types.adoc[Special Bean Types] and xref:web/webmvc/mvc-servlet/config.adoc[Web MVC Config]. - - - 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 b4f501e7331c..4a79db746df1 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 @@ -17,6 +17,3 @@ 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 the classpath. - - - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc new file mode 100644 index 000000000000..0f221f33a8ee --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc @@ -0,0 +1,41 @@ +[[mvc-config-api-version]] += API Version + +[.small]#xref:web/webflux/config.adoc#webflux-config-api-version[See equivalent in the Reactive stack]# + +To enable API versioning, use the `ApiVersionConfigurer` callback of `WebMvcConfigurer`: + +include-code::./WebConfiguration[tag=snippet,indent=0] + +You can resolve the version through one of the built-in options listed below, or +alternatively use a custom `ApiVersionResolver`: + +- Request header +- Request parameter +- Path segment +- Media type parameter + +To resolve from a path segment, you need to specify the index of the path segment expected +to contain the version. The path segment must be declared as a URI variable, e.g. +"/\{version}", "/api/\{version}", etc. where the actual name is not important. +As the version is typically at the start of the path, consider configuring it externally +as a common path prefix for all handlers through the +xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options. + +By default, the version is parsed with `SemanticVersionParser`, but you can also configure +a custom xref:web/webmvc-versioning.adoc#mvc-versioning-parser[ApiVersionParser]. + +Supported versions are transparently detected from versions declared in request mappings +for convenience, but you can turn that off through a flag in the MVC config, and +consider only the versions configured explicitly in the config as supported. +Requests with a version that is not supported are rejected with +`InvalidApiVersionException` resulting in a 400 response. + +You can set an `ApiVersionDeprecationHandler` to send information about deprecated +versions to clients. The built-in standard handler can set "Deprecation", "Sunset", and +"Link" headers based on https://datatracker.ietf.org/doc/html/rfc9745[RFC 9745] and +https://datatracker.ietf.org/doc/html/rfc8594[RFC 8594]. + +Once API versioning is configured, you can begin to map requests to +xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[controller methods] +according to the request version. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc index 3850a9931ba1..abe246e508aa 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 @@ -10,12 +10,10 @@ By default, only the `Accept` header is checked. If you must use URL-based content type resolution, consider using the query parameter 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 +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. You can customize requested content type resolution, as the following example shows: 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 2e8286fa8974..91e7cef26c21 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,7 +4,8 @@ [.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, use the following: @@ -20,6 +21,3 @@ 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 FormatterRegistrar implementations. - - - 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 a42ea1388a44..596295044b58 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 @@ -12,6 +12,3 @@ In XML, you can check attributes and sub-elements of ``. view the https://schema.spring.io/mvc/spring-mvc.xsd[Spring MVC XML schema] or use the code completion feature of your IDE to discover what attributes and sub-elements are available. - - - 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 2ad51145d66b..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 @@ -21,7 +21,7 @@ The caveat to overriding the `/` Servlet mapping is that the `RequestDispatcher` 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: 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 87d2ab0a0a70..15892142aafe 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/enable.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/enable.adoc @@ -7,11 +7,12 @@ You can use the `@EnableWebMvc` annotation to enable MVC configuration with prog include-code::./WebConfiguration[tag=snippet,indent=0] -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}/web.html#web.servlet.spring-mvc.auto-configuration[the dedicated Spring Boot documentation]. +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 available on the classpath (for example, payload converters for JSON, XML, and others). - - - 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 ad09392ec7a5..1535f411714b 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 @@ -3,48 +3,10 @@ [.small]#xref:web/webflux/config.adoc#webflux-config-message-codecs[See equivalent in the Reactive stack]# -You can set the `HttpMessageConverter` instances to use in Java configuration, -replacing the ones used by default, by overriding -{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 -{spring-framework-api}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`]. +You can configure the `HttpMessageConverter` instances to use by overriding +{spring-framework-api}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters(org.springframework.http.converter.HttpMessageConverters.Builder)[`configureMessageConverters()`]. -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 {spring-boot-docs}/web.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 configures custom Jackson JSON and XML converters with customized mappers instead of the default +ones: include-code::./WebConfiguration[tag=snippet,indent=0] - -In the preceding example, -{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 -{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: - -* {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: - -* {jackson-github-org}/jackson-datatype-joda[jackson-datatype-joda]: Support for Joda-Time types. -* {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`] -dependency in addition to https://search.maven.org/#search%7Cga%7C1%7Ca%3A%22jackson-dataformat-xml%22[`jackson-dataformat-xml`] one. - -Other interesting Jackson modules are available: - -* https://github.com/zalando/jackson-datatype-money[jackson-datatype-money]: Support for `javax.money` types (unofficial module). -* {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/static-resources.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc index 008832cfd954..f99633545081 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc @@ -48,17 +48,13 @@ 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 b867977160fd..5597e5c38f48 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 @@ -19,7 +19,5 @@ example shows: 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. - - - +mark it with `@Primary`, or mark the one declared in the MVC configuration with +`@Fallback`, in order to avoid conflict. 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 47d803b10c80..5d60781ec67d 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 @@ -15,6 +15,3 @@ annotated controller is considered a strong enough indication of endpoint owners that a 405 (METHOD_NOT_ALLOWED), a 415 (UNSUPPORTED_MEDIA_TYPE), or similar response can be sent to the client to help with debugging. For this reason it is recommended to avoid splitting URL handling across an annotated controller and a view controller. - - - 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 e893eaa75832..4e7163ea6306 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 @@ -49,6 +49,3 @@ but many other options exist and are explained later in this chapter. 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 90f206a7c44d..2e3ec0fd0864 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-advice.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-advice.adoc @@ -10,24 +10,26 @@ to any controller. Moreover, as of 5.3, `@ExceptionHandler` methods in `@Control can be used to handle exceptions from any `@Controller` or any other handler. `@ControllerAdvice` is meta-annotated with `@Component` and therefore can be registered as -a Spring bean through xref:core/beans/java/instantiating-container.adoc#beans-java-instantiating-container-scan[component scanning] -. `@RestControllerAdvice` is meta-annotated with `@ControllerAdvice` -and `@ResponseBody`, and that means `@ExceptionHandler` methods will have their return -value rendered via response body message conversion, rather than via HTML views. +a Spring bean through xref:core/beans/java/instantiating-container.adoc#beans-java-instantiating-container-scan[component scanning]. + +`@RestControllerAdvice` is a shortcut annotation that combines `@ControllerAdvice` +with `@ResponseBody`, in effect simply an `@ControllerAdvice` whose exception handler +methods render to the response body. On startup, `RequestMappingHandlerMapping` and `ExceptionHandlerExceptionResolver` detect controller advice beans and apply them at runtime. Global `@ExceptionHandler` methods, from an `@ControllerAdvice`, are applied _after_ local ones, from the `@Controller`. By contrast, global `@ModelAttribute` and `@InitBinder` methods are applied _before_ local ones. -The `@ControllerAdvice` annotation has attributes that let you narrow the set of controllers -and handlers that they apply to. For example: +By default, both `@ControllerAdvice` and `@RestControllerAdvice` apply to any controller, +including `@Controller` and `@RestController`. Use attributes of the annotation to narrow +the set of controllers and handlers that they apply to. For example: [tabs] ====== 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 +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,7 +66,3 @@ The selectors in the preceding example are evaluated at runtime and may negative performance if used extensively. See the {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 08c7961c15b0..49c0fed3a5d6 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,15 +6,14 @@ `@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: - include-code::./SimpleController[indent=0] [[mvc-ann-exceptionhandler-exc]] == Exception Mapping -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. +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. @@ -74,7 +73,6 @@ 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]# @@ -177,13 +175,9 @@ the content negotiation during the error handling phase will decide which conten be converted through `HttpMessageConverter` instances and written to the response. See xref:web/webmvc/mvc-controller/ann-methods/responseentity.adoc[ResponseEntity]. -| `ErrorResponse` -| To render an RFC 9457 error response with details in the body, -see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] - -| `ProblemDetail` +| `ErrorResponse`, `ProblemDetail` | To render an RFC 9457 error response with details in the body, -see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] + see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | `String` | A view name to be resolved with `ViewResolver` implementations and used together with the @@ -194,7 +188,7 @@ see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] | `View` | A `View` instance to use for rendering together with the implicit model -- determined through command objects and `@ModelAttribute` methods. The handler method may also - programmatically enrich the model by declaring a `Model` argument (descried earlier). + programmatically enrich the model by declaring a `Model` argument (described earlier). | `java.util.Map`, `org.springframework.ui.Model` | Attributes to be added to the implicit model with the view name implicitly determined @@ -221,10 +215,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 - {spring-framework-api}/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..86a65b471d19 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 { @@ -105,10 +105,6 @@ Kotlin:: <1> Defining an `@InitBinder` method on a custom formatter. ====== -[[mvc-ann-initbinder-model-design]] -== Model Design -[.small]#xref:web/webflux/controller/ann-initbinder.adoc#webflux-ann-initbinder-model-design[See equivalent in the Reactive stack]# - -include::partial$web/web-data-binding-model-design.adoc[] - +[[mvc-ann-initbinder-model-design]] +NOTE: For more guidance on model design, please see xref:web/webmvc/mvc-data-binding.adoc[Data Binding]. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods.adoc index 853e26607d90..069a277d4046 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods.adoc @@ -6,5 +6,3 @@ `@RequestMapping` handler methods have a flexible signature and can choose from a range of supported controller method arguments and return values. - - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/arguments.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/arguments.adoc index 4e3b30ea3c09..9b82c91d8939 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 @@ -6,7 +6,7 @@ The next table describes the supported controller method arguments. Reactive types are not supported for any arguments. -JDK 8's `java.util.Optional` is supported as a method argument in combination with +`java.util.Optional` is supported as a method argument in combination with annotations that have a `required` attribute (for example, `@RequestParam`, `@RequestHeader`, and others) and is equivalent to `required=false`. @@ -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,8 +134,6 @@ 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 - {spring-framework-api}/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..473a697f0cc0 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> @@ -42,5 +42,3 @@ Kotlin:: If the target method parameter type is not `String`, type conversion is applied automatically. See xref:web/webmvc/mvc-controller/ann-methods/typeconversion.adoc[Type Conversion]. - - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/flash-attributes.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/flash-attributes.adoc index b4ecd70d0276..92a41c5a715d 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/flash-attributes.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/flash-attributes.adoc @@ -42,5 +42,3 @@ This does not entirely eliminate the possibility of a concurrency issue but reduces it greatly with information that is already available in the redirect URL. Therefore, we recommend that you use flash attributes mainly for redirect scenarios. **** - - 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..2d3d2f3c9783 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) { @@ -28,6 +28,3 @@ Kotlin:: } ---- ====== - - - 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 b0522fdb773c..9161c5a61fe4 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 @@ -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() { @@ -158,6 +158,3 @@ Kotlin:: } ---- ====== - - - 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 c96b15a35297..2834af4587cf 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 @@ -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,5 @@ 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 1ad2640d2abb..52ee83c4f0e4 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> @@ -74,7 +78,7 @@ Kotlin:: ====== By default, both constructor and property -xref:core/validation/beans-beans.adoc#beans-binding[data binding] are applied. However, +xref:core/validation/data-binding.adoc[data binding] are applied. However, model object design requires careful consideration, and for security reasons it is recommended either to use an object tailored specifically for web binding, or to apply constructor binding only. If property binding must still be used, then _allowedFields_ @@ -89,11 +93,11 @@ 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 { - private final String firstName; + private final String firstName; public Account(@BindParam("first-name") String firstName) { this.firstName = firstName; @@ -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> @@ -235,14 +243,14 @@ Kotlin:: ====== If there is no `BindingResult` parameter after the `@ModelAttribute`, then -`MethodArgumentNotValueException` is raised with the validation errors. However, if method +a `MethodArgumentNotValidException` is raised with the validation errors. However, if method validation applies because other parameters have `@jakarta.validation.Constraint` annotations, then `HandlerMethodValidationException` is raised instead. For more details, see the section xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]. TIP: Using `@ModelAttribute` is optional. By default, any parameter that is not a simple value type as determined by -{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty] +{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` 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..c2f4386df728 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 { @@ -207,5 +207,3 @@ Kotlin:: If method validation applies because other parameters have `@Constraint` annotations, then `HandlerMethodValidationException` is raised instead. For more details, see the section on xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]. - - 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..3361914edbf5 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 { @@ -51,8 +51,7 @@ Kotlin:: ---- ====== -Another way of passing data to the redirect target is by using flash attributes. Unlike -other redirect attributes, flash attributes are saved in the HTTP session (and, hence, do -not appear in the URL). See xref:web/webmvc/mvc-controller/ann-methods/flash-attributes.adoc[Flash Attributes] for more information. - - +Another way of passing data to the redirect target is by using flash attributes. Unlike other +redirect attributes, flash attributes are saved in the HTTP session (and, hence, do not appear +in the URL). See xref:web/webmvc/mvc-controller/ann-methods/flash-attributes.adoc[Flash Attributes] +for more information. 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..3e1edf25f15f 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> @@ -31,5 +31,3 @@ Kotlin:: ---- <1> Using the `@RequestAttribute` annotation. ====== - - 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..781038a3b751 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) { @@ -30,9 +30,13 @@ 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. -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. @@ -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) { @@ -67,4 +71,3 @@ Kotlin:: If method validation applies because other parameters have `@Constraint` annotations, then `HandlerMethodValidationException` is raised instead. For more details, see the section on xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]. - 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..d6c00e5f24a8 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( @@ -52,8 +52,8 @@ Kotlin:: <2> Get the value of the `Keep-Alive` header. ====== -If the target method parameter type is not -`String`, type conversion is automatically applied. See xref:web/webmvc/mvc-controller/ann-methods/typeconversion.adoc[Type Conversion]. +If the target method parameter type is not `String`, type conversion is automatically applied. +See xref:web/webmvc/mvc-controller/ann-methods/typeconversion.adoc[Type Conversion]. When an `@RequestHeader` annotation is used on a `Map`, `MultiValueMap`, or `HttpHeaders` argument, the map is populated @@ -63,5 +63,3 @@ TIP: Built-in support is available for converting a comma-separated string into array or collection of strings or other types known to the type conversion system. For example, a method parameter annotated with `@RequestHeader("Accept")` can be of type `String` but also `String[]` or `List`. - - 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 3486beeb3b07..b443c47e9117 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,11 +72,51 @@ 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 -{spring-framework-api}/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 4fb18b0002cf..02bfeb1f2aad 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 @@ -42,15 +42,14 @@ 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` -(e.g. through a lambda expression that retrieves the actual `InputStream`). +(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. +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. -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. +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. You can combine `@ResponseBody` methods with JSON serialization views. See xref:web/webmvc/mvc-controller/ann-methods/jackson.adoc[Jackson JSON] for 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 2baae5ae7677..eb2e13731ae7 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 @@ -3,13 +3,14 @@ [.small]#xref:web/webflux/controller/ann-methods/responseentity.adoc[See equivalent in the Reactive stack]# -`ResponseEntity` is like xref:web/webmvc/mvc-controller/ann-methods/responsebody.adoc[`@ResponseBody`] but with status and headers. For example: +`ResponseEntity` is like xref:web/webmvc/mvc-controller/ann-methods/responsebody.adoc[`@ResponseBody`] +but with status and headers. For example: [tabs] ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/something") public ResponseEntity handle() { @@ -21,7 +22,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @GetMapping("/something") fun handle(): ResponseEntity { @@ -33,14 +34,16 @@ Kotlin:: ====== The body will usually be provided as a value object to be rendered to a corresponding -response representation (e.g. JSON) by one of the registered `HttpMessageConverters`. +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` -(e.g. through a lambda expression that retrieves the actual `InputStream`). +(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 @@ -52,5 +55,3 @@ types for the body. This allows the following types of async responses: * `Mono>` provides all three -- response status, headers, and body, asynchronously at a later point. This allows the response status and headers to vary depending on the outcome of asynchronous request handling. - - 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 00d9f862428e..ed8fc25e875f 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/return-types.adoc @@ -22,11 +22,7 @@ supported for all return values. | `HttpHeaders` | For returning a response with headers and no body. -| `ErrorResponse` -| To render an RFC 9457 error response with details in the body, - see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] - -| `ProblemDetail` +| `ErrorResponse`, `ProblemDetail` | To render an RFC 9457 error response with details in the body, see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] @@ -56,6 +52,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,17 +89,15 @@ 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 - {spring-framework-api}/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..3e32bee6e541 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> @@ -40,5 +40,3 @@ For use cases that require adding or removing session attributes, consider injec For temporary storage of model attributes in the session as part of a controller workflow, consider using `@SessionAttributes` as described in xref:web/webmvc/mvc-controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`]. - - 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..bff111b04a84 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> @@ -91,5 +91,3 @@ class EditPetForm { <1> Storing the `Pet` value in the Servlet session. <2> Clearing the `Pet` value from the Servlet session. ====== - - 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..dc122a903a5d 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,9 +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 c034514f2760..0529708ea4fc 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 { @@ -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") @@ -112,6 +112,3 @@ Kotlin:: } ---- ====== - - - 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 fe929fda35e7..6a83a1b00e1b 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 @@ -6,7 +6,6 @@ This section discusses request mapping for annotated controllers. - [[mvc-ann-requestmapping-annotation]] == `@RequestMapping` @@ -25,9 +24,10 @@ There are also HTTP method specific shortcut variants of `@RequestMapping`: * `@DeleteMapping` * `@PatchMapping` -The shortcuts are xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-composed[Custom Annotations] that 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. +The shortcuts are +xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-composed[Custom Annotations] +that 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. A `@RequestMapping` is still needed at the class level to express shared mappings. NOTE: `@RequestMapping` cannot be used in conjunction with other `@RequestMapping` @@ -42,7 +42,7 @@ The following example has type and method level mappings: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/persons") @@ -63,7 +63,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @RestController @RequestMapping("/persons") @@ -84,42 +84,21 @@ Kotlin:: ====== - [[mvc-ann-requestmapping-uri-templates]] == URI patterns [.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-uri-templates[See equivalent in the Reactive stack]# -`@RequestMapping` methods can be mapped using URL patterns. There are two alternatives: +`@RequestMapping` methods can be mapped using URL patterns. +Spring MVC is using `PathPattern` -- a pre-parsed pattern matched against the URL path also pre-parsed as `PathContainer`. +Designed for web use, this solution deals effectively with encoding and path parameters, and matches efficiently. +See xref:web/webmvc/mvc-config/path-matching.adoc[MVC config] for customizations of path matching options. -* `PathPattern` -- a pre-parsed pattern matched against the URL path also pre-parsed as -`PathContainer`. Designed for web use, this solution deals effectively with encoding and -path parameters, and matches efficiently. -* `AntPathMatcher` -- match String patterns against a String path. This is the original -solution also used in Spring configuration to select resources on the classpath, on the -filesystem, and other locations. It is less efficient and the String path input is a +NOTE: the `AntPathMatcher` variant is now deprecated because it is less efficient and the String path input is a challenge for dealing effectively with encoding and other issues with URLs. -`PathPattern` is the recommended solution for web applications and it is the only choice in -Spring WebFlux. It was enabled for use in Spring MVC from version 5.3 and is enabled by -default from version 6.0. See xref:web/webmvc/mvc-config/path-matching.adoc[MVC config] for -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 -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 -{spring-framework-api}/web/util/pattern/PathPattern.html[PathPattern] and -{spring-framework-api}/util/AntPathMatcher.html[AntPathMatcher]. +You can map requests by using glob patterns and wildcards: -Some example patterns: - -* `+"/resources/ima?e.png"+` - match one character in a path segment -* `+"/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 +include::partial$web/uri-patterns.adoc[leveloffset=+1] Captured URI variables can be accessed with `@PathVariable`. For example: @@ -127,7 +106,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) { @@ -137,7 +116,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 { @@ -153,7 +132,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}") @@ -168,7 +147,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @Controller @RequestMapping("/owners/{ownerId}") @@ -199,7 +178,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) { @@ -209,7 +188,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) { @@ -218,11 +197,13 @@ Kotlin:: ---- ====== -URI path patterns can also have embedded `${...}` placeholders that are resolved on startup -by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and -other property sources. You can use this, for example, to parameterize a base URL based on -some external configuration. +URI path patterns can also have: +- Embedded `${...}` placeholders that are resolved on startup via +`PropertySourcesPlaceholderConfigurer` against local, system, environment, and +other property sources. This is useful, for example, to parameterize a base URL based on +external configuration. +- SpEL expression `#{...}`. [[mvc-ann-requestmapping-pattern-comparison]] @@ -233,7 +214,7 @@ When multiple patterns match a URL, the best match must be selected. This is don one of the following depending on whether use of parsed `PathPattern` is enabled for use or not: * {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)`] +* {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), @@ -248,36 +229,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 @@ -317,7 +268,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) { @@ -328,7 +279,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) { @@ -360,7 +311,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 @@ -372,7 +323,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 @@ -406,7 +357,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) { @@ -417,7 +368,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) { @@ -433,7 +384,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) { @@ -444,7 +395,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) { @@ -455,10 +406,98 @@ Kotlin:: ====== TIP: You can match `Content-Type` and `Accept` with the headers condition, but it is better to use -xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-consumes[consumes] and xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-produces[produces] +xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-consumes[consumes] +and xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-produces[produces] instead. +[[mvc-ann-requestmapping-version]] +== API Version +[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[See equivalent in the Reactive stack]# + +There is no standard way to specify an API version, so when you enable API versioning +in the xref:web/webmvc/mvc-config/api-version.adoc[MVC Config] you need +to specify how to resolve the version. The MVC Config creates an +xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[ApiVersionStrategy] that in turn +is used to map requests. + +Once API versioning is enabled, you can begin to map requests with versions. +The `@RequestMapping` `version` attribute supports the following: + +- Fixed version ("1.2") -- matches the given version only +- Baseline version ("1.2+") -- matches the given and xref:web/webmvc/mvc-config/api-version.adoc[supported versions] above +- No value -- matches any version, but is superseded by a more specific version match + +If multiple controller methods have a version less than or equal to the request version, +the highest of those, and closest to the request version, is the one considered, +in effect superseding the rest. + +To illustrate this, consider the following mappings: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @RestController + @RequestMapping("/account/{id}") + public class AccountController { + + @GetMapping // <1> + public Account getAccount() { + } + + @GetMapping(version = "1.1") // <2> + public Account getAccount1_1() { + } + + @GetMapping(version = "1.2+") // <3> + public Account getAccount1_2() { + } + + @GetMapping(version = "1.5") // <4> + public Account getAccount1_5() { + } + } +---- +<1> match any version +<2> match version 1.1 +<3> match version 1.2 and supported versions above +<4> match version 1.5 +====== + +For request with version `"1.3"`: + +- (1) matches as it matches any version +- (2) does not match +- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match +- (4) is higher and does not match + +NOTE: Version 1.3 must be present in the mappings, or be +xref:web/webmvc/mvc-config/api-version.adoc[configured as supported]. + +For request with version `"1.5"`: + +- (1) matches as it matches any version +- (2) does not match +- (3) matches as it matches 1.2 and above +- (4) matches and is *chosen* as the highest match + +A request with version `"1.6"` does not have a match. (1) and (3) do match, but are +superseded by (4), which allows only a strict match, and therefore does not match. +In this scenario, a `NotAcceptableApiVersionException` results in a 400 response. + +Controller methods without a version are intended to support clients created before a +versioned alternative was introduced. Therefore, even though an unversioned controller +method is considered a match for any version, it is in fact given the lowest priority, +and is effectively superseded by any alternative controller method with a version. + +See xref:web/webmvc-versioning.adoc[API Versioning] for more details on underlying +infrastructure and support for API Versioning. + + + [[mvc-ann-requestmapping-head-options]] == HTTP HEAD, OPTIONS [.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-head-options[See equivalent in the Reactive stack]# @@ -515,53 +554,7 @@ You can programmatically register handler methods, which you can use for dynamic registrations or for advanced cases, such as different instances of the same handler under different URLs. The following example registers a handler method: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - public class MyConfig { - - @Autowired - public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) // <1> - throws NoSuchMethodException { - - RequestMappingInfo info = RequestMappingInfo - .paths("/user/{id}").methods(RequestMethod.GET).build(); // <2> - - Method method = UserHandler.class.getMethod("getUser", Long.class); // <3> - - mapping.registerMapping(info, handler, method); // <4> - } - } ----- -<1> Inject the target handler and the handler mapping for controllers. -<2> Prepare the request mapping meta data. -<3> Get the handler method. -<4> Add the registration. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - class MyConfig { - - @Autowired - fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { // <1> - val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() // <2> - val method = UserHandler::class.java.getMethod("getUser", Long::class.java) // <3> - mapping.registerMapping(info, handler, method) // <4> - } - } ----- -<1> Inject the target handler and the handler mapping for controllers. -<2> Prepare the request mapping meta data. -<3> Get the handler method. -<4> Add the registration. -====== +include-code::./MyConfiguration[tag=snippet,indent=0] @@ -570,10 +563,9 @@ Kotlin:: [.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-httpexchange-annotation[See equivalent in the Reactive 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 +generated proxy, the 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 xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service Client] 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. @@ -587,7 +579,7 @@ For example: ====== Java:: + -[source,java,indent=0,subs="verbatim,quotes",role="primary"] +[source,java,indent=0,subs="verbatim,quotes"] ---- @HttpExchange("/persons") interface PersonService { @@ -615,7 +607,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- @HttpExchange("/persons") interface PersonService { @@ -650,5 +642,10 @@ 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:integration/rest-clients.adoc#rest-http-service-client-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 0cbe9c3d06a5..45e5c632f4b7 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 @@ -7,22 +7,26 @@ 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: -1. xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], +1. Java Bean Validation is applied individually to an +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] 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 +xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] method parameter +annotated with `@jakarta.validation.Valid` or Spring's `@Validated` so long as +it is a command object rather than a container such as `Map` or `Collection`, it does not +have `Errors` or `BindingResult` immediately after in the method signature, and does not +otherwise require method validation (see next). `MethodArgumentNotValidException` is the +exception raised when validating a method parameter individually. + +2. Java Bean Validation is applied to the method when `@Constraint` annotations such as +`@Min`, `@NotBlank` and others are declared directly on method parameters, or on the +method for the return value, and it supersedes any validation that would be applied +otherwise to a method parameter individually because method validation covers both +method parameter constraints and nested constraints via `@Valid`. +`HandlerMethodValidationException` is the exception raised validation is applied +to the method. + +Applications should handle both `MethodArgumentNotValidException` and +`HandlerMethodValidationException` since 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. @@ -57,7 +61,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: @@ -65,7 +69,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 = ... ; @@ -73,12 +77,12 @@ Java:: @Override public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) { - // ... + // ... } @Override public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) { - // ... + // ... } @Override @@ -88,14 +92,14 @@ Java:: @Override public void other(ParameterValidationResult result) { - // ... + // ... } }); ---- Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- // HandlerMethodValidationException val ex @@ -103,22 +107,22 @@ Kotlin:: ex.visitResults(object : HandlerMethodValidationException.Visitor { override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) { - // ... - } + // ... + } override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) { - // ... - } + // ... + } override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) { - // ... - } + // ... + } // ... override fun other(result: ParameterValidationResult) { - // ... - } + // ... + } }) ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc index 493d1d74d5f2..d95c9b0fd62b 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 @@ -39,6 +39,3 @@ NOTE: Keep in mind that as of 6.0, with interface proxying, Spring MVC no longer controllers based solely on a type-level `@RequestMapping` annotation on the interface. Please, enable class based proxying, or otherwise the interface must also have an `@Controller` annotation. - - - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-data-binding.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-data-binding.adoc new file mode 100644 index 000000000000..e8d581630b42 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-data-binding.adoc @@ -0,0 +1,34 @@ +[[mvc-data-binding]] += Data Binding +:page-section-summary-toc: 1 + +[.small]#xref:web/webflux/data-binding.adoc[See equivalent in the Reactive stack]# + +Data binding is a mechanism that binds string parameters onto an object graph with type conversion. +It is a core mechanism of the Spring Framework that helps with application configuration. +In web applications it makes it easy to access query parameters and form data through richly typed objects rather than through maps of string values. + +To learn more about the data binding mechanism, including constructor and setter binding, property name syntax, type conversion, +and more, see xref:core/validation/data-binding.adoc[Data binding] in the Core Technologies section. + +For annotated controllers, data binding applies to a +xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute] method argument. +For functional endpoints, use the `bind` method of xref:web/webmvc-functional.adoc#webmvc-fn-request[ServerRequest]. + +TIP: For browser applications with annotated controllers, you can use +xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute methods] +to initialize additional model attributes for use in rendered views. + +Each request uses a separate `WebDataBinder` instance. +For annotated controllers, this instance can be customized through +xref:web/webmvc/mvc-controller/ann-initbinder.adoc[@InitBinder methods] within a controller, or +across controllers through xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice]. +For functional endpoints, use overloaded `ServerRequest.bind` methods. + + + +[[mvc-data-binding-design]] +== Model Design +[.small]#xref:web/webflux/data-binding.adoc#webflux-data-binding-design[See equivalent in the Reactive stack]# + +include::partial$web/web-data-binding-model-design.adoc[] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc index da03fba9b4a1..e5e19ea705a0 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc @@ -4,12 +4,8 @@ [.small]#xref:web/webflux/http2.adoc[See equivalent in the Reactive stack]# -Servlet 4 containers are required to support HTTP/2, and Spring Framework 5 is compatible -with Servlet API 4. From a programming model perspective, there is nothing specific that +Servlet 4 containers are required to support HTTP/2, and Spring Framework requires +Servlet API 6.1. From a programming model perspective, there is nothing specific that applications need to do. However, there are considerations related to server configuration. For more details, see the {spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page]. - -The Servlet API does expose one construct related to HTTP/2. You can use the -`jakarta.servlet.http.PushBuilder` to proactively push resources to clients, and it -is supported as a xref:web/webmvc/mvc-controller/ann-methods/arguments.adoc[method argument] to `@RequestMapping` methods. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-range.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-range.adoc new file mode 100644 index 000000000000..7ba60ead5379 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-range.adoc @@ -0,0 +1,23 @@ +[[mvc-range]] += Range Requests +:page-section-summary-toc: 1 + +[.small]#xref:web/webflux/range.adoc[See equivalent in the Reactive stack]# + +Spring MVC supports https://datatracker.ietf.org/doc/html/rfc9110#section-14[RFC 9110] +range requests. For an overview, see the +https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests[Ranger Requests] +Mozilla guide. + +The `Range` header is parsed and handled transparently in Spring MVC when an annotated +controller returns a `Resource` or `ResponseEntity`, or a functional endpoint +xref:web/webmvc-functional.adoc#webmvc-fn-resources[serves a `Resource`]. `Range` header +support is also transparently handled when serving +xref:web/webmvc/mvc-config/static-resources.adoc[static resources]. + +TIP: The `Resource` must not be an `InputStreamResource` and with `ResponseEntity`, +the status of the response must be 200. + +The underlying support is in the `HttpRange` class, which exposes methods to parse +`Range` headers and split a `Resource` into a `List` that in turn can be +then written to the response via `ResourceRegionHttpMessageConverter`. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc index 446ae42c0414..50143f08a065 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc @@ -13,8 +13,4 @@ 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 189ae02988e1..1374c3dd3381 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet.adoc @@ -14,55 +14,12 @@ In turn, the `DispatcherServlet` uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, xref:web/webmvc/mvc-servlet/special-bean-types.adoc[and more]. -The following example of the Java configuration registers and initializes +The following example shows the programmatic registration and initialization of the `DispatcherServlet`, which is auto-detected by the Servlet container -(see xref:web/webmvc/mvc-servlet/container-config.adoc[Servlet Config]): +(see xref:web/webmvc/mvc-servlet/container-config.adoc[Servlet Config]), and the +equivalent `web.xml`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class MyWebApplicationInitializer implements WebApplicationInitializer { - - @Override - public void onStartup(ServletContext servletContext) { - - // Load Spring web application configuration - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.register(AppConfig.class); - - // Create and register the DispatcherServlet - DispatcherServlet servlet = new DispatcherServlet(context); - ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet); - registration.setLoadOnStartup(1); - registration.addMapping("/app/*"); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class MyWebApplicationInitializer : WebApplicationInitializer { - - override fun onStartup(servletContext: ServletContext) { - - // Load Spring web application configuration - val context = AnnotationConfigWebApplicationContext() - context.register(AppConfig::class.java) - - // Create and register the DispatcherServlet - val servlet = DispatcherServlet(context) - val registration = servletContext.addServlet("app", servlet) - registration.setLoadOnStartup(1) - registration.addMapping("/app/*") - } - } ----- -====== +include-code::./MyWebApplicationInitializer[tag=snippet,indent=0] NOTE: In addition to using the ServletContext API directly, you can also extend `AbstractAnnotationConfigDispatcherServletInitializer` and override specific methods @@ -73,45 +30,9 @@ alternative to `AnnotationConfigWebApplicationContext`. See the {spring-framework-api}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`] javadoc for details. -The following example of `web.xml` configuration registers and initializes the `DispatcherServlet`: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - org.springframework.web.context.ContextLoaderListener - - - - contextConfigLocation - /WEB-INF/app-context.xml - - - - app - org.springframework.web.servlet.DispatcherServlet - - contextConfigLocation - - - 1 - - - - app - /app/* - - - ----- - NOTE: Spring Boot follows a different initialization sequence. Rather than hooking into 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 -{spring-boot-docs}/web.html#web.servlet.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/container-config.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/container-config.adoc index a6a4755c0230..ae6f4eb34e73 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 @@ -5,48 +5,7 @@ In a Servlet environment, you have the option of configuring the Servlet contain programmatically as an alternative or in combination with a `web.xml` file. The following example registers a `DispatcherServlet`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - import org.springframework.web.WebApplicationInitializer; - - public class MyWebApplicationInitializer implements WebApplicationInitializer { - - @Override - public void onStartup(ServletContext container) { - XmlWebApplicationContext appContext = new XmlWebApplicationContext(); - appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml"); - - ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext)); - registration.setLoadOnStartup(1); - registration.addMapping("/"); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - import org.springframework.web.WebApplicationInitializer - - class MyWebApplicationInitializer : WebApplicationInitializer { - - override fun onStartup(container: ServletContext) { - val appContext = XmlWebApplicationContext() - appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml") - - val registration = container.addServlet("dispatcher", DispatcherServlet(appContext)) - registration.setLoadOnStartup(1) - registration.addMapping("/") - } - } ----- -====== - +include-code::./MyWebApplicationInitializer[tag=snippet,indent=0] `WebApplicationInitializer` is an interface provided by Spring MVC that ensures your implementation is detected and automatically used to initialize any Servlet 3 container. @@ -55,144 +14,21 @@ An abstract base class implementation of `WebApplicationInitializer` named `DispatcherServlet` by overriding methods to specify the servlet mapping and the location of the `DispatcherServlet` configuration. -This is recommended for applications that use Java-based Spring configuration, as the +This is recommended for applications that use programmatic Spring configuration, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - - @Override - protected Class[] getRootConfigClasses() { - return null; - } - - @Override - protected Class[] getServletConfigClasses() { - return new Class[] { MyWebConfig.class }; - } - - @Override - protected String[] getServletMappings() { - return new String[] { "/" }; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { - - override fun getRootConfigClasses(): Array>? { - return null - } - - override fun getServletConfigClasses(): Array>? { - return arrayOf(MyWebConfig::class.java) - } - - override fun getServletMappings(): Array { - return arrayOf("/") - } - } ----- -====== +include-code::./MyWebAppInitializer[tag=snippet,indent=0] If you use XML-based Spring configuration, you should extend directly from `AbstractDispatcherServletInitializer`, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class MyWebAppInitializer extends AbstractDispatcherServletInitializer { - - @Override - protected WebApplicationContext createRootApplicationContext() { - return null; - } - - @Override - protected WebApplicationContext createServletApplicationContext() { - XmlWebApplicationContext cxt = new XmlWebApplicationContext(); - cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml"); - return cxt; - } - - @Override - protected String[] getServletMappings() { - return new String[] { "/" }; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class MyWebAppInitializer : AbstractDispatcherServletInitializer() { - - override fun createRootApplicationContext(): WebApplicationContext? { - return null - } - - override fun createServletApplicationContext(): WebApplicationContext { - return XmlWebApplicationContext().apply { - setConfigLocation("/WEB-INF/spring/dispatcher-config.xml") - } - } - - override fun getServletMappings(): Array { - return arrayOf("/") - } - } ----- -====== +include-code::./MyXmlDispatcherServletInitializer[tag=snippet,indent=0] `AbstractDispatcherServletInitializer` also provides a convenient way to add `Filter` instances and have them be automatically mapped to the `DispatcherServlet`, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class MyWebAppInitializer extends AbstractDispatcherServletInitializer { - - // ... - - @Override - protected Filter[] getServletFilters() { - return new Filter[] { - new HiddenHttpMethodFilter(), new CharacterEncodingFilter() }; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class MyWebAppInitializer : AbstractDispatcherServletInitializer() { - - // ... - - override fun getServletFilters(): Array { - return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter()) - } - } ----- -====== +include-code::./MyFilterDispatcherServletInitializer[tag=snippet,indent=0] Each filter is added with a default name based on its concrete type and automatically mapped to the `DispatcherServlet`. 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..e1aabb0d1111 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 @@ -20,91 +20,15 @@ are effectively inherited and can be overridden (that is, re-declared) in the Se child `WebApplicationContext`, which typically contains beans local to the given `Servlet`. The following image shows this relationship: -image::mvc-context-hierarchy.png[] +image::mvc-context-hierarchy.png[width=60%,align="center"] -The following example configures a `WebApplicationContext` hierarchy: +The following example configures a `WebApplicationContext` hierarchy, and the equivalent `web.xml`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - - @Override - protected Class[] getRootConfigClasses() { - return new Class[] { RootConfig.class }; - } - - @Override - protected Class[] getServletConfigClasses() { - return new Class[] { App1Config.class }; - } - - @Override - protected String[] getServletMappings() { - return new String[] { "/app1/*" }; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { - - override fun getRootConfigClasses(): Array> { - return arrayOf(RootConfig::class.java) - } - - override fun getServletConfigClasses(): Array> { - return arrayOf(App1Config::class.java) - } - - override fun getServletMappings(): Array { - return arrayOf("/app1/*") - } - } ----- -====== +include-code::./MyWebAppInitializer[tag=snippet,indent=0] TIP: If an application context hierarchy is not required, applications can return all configuration through `getRootConfigClasses()` and `null` from `getServletConfigClasses()`. -The following example shows the `web.xml` equivalent: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - org.springframework.web.context.ContextLoaderListener - - - - contextConfigLocation - /WEB-INF/root-context.xml - - - - app1 - org.springframework.web.servlet.DispatcherServlet - - contextConfigLocation - /WEB-INF/app1-context.xml - - 1 - - - - app1 - /app1/* - - - ----- TIP: If an application context hierarchy is not required, applications may configure a "`root`" context only and leave the `contextConfigLocation` Servlet parameter empty. 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 01cab7f3b7ed..3a0d285b02e9 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 @@ -74,42 +74,7 @@ Servlet container makes an ERROR dispatch within the container to the configured to a `@Controller`, which could be implemented to return an error view name with a model or to render a JSON response, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @RestController - public class ErrorController { - - @RequestMapping(path = "/error") - public Map handle(HttpServletRequest request) { - Map map = new HashMap<>(); - map.put("status", request.getAttribute("jakarta.servlet.error.status_code")); - map.put("reason", request.getAttribute("jakarta.servlet.error.message")); - return map; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @RestController - class ErrorController { - - @RequestMapping(path = ["/error"]) - fun handle(request: HttpServletRequest): Map { - val map = HashMap() - map["status"] = request.getAttribute("jakarta.servlet.error.status_code") - map["reason"] = request.getAttribute("jakarta.servlet.error.message") - return map - } - } ----- -====== +include-code::./ErrorController[tag=snippet,indent=0] TIP: The Servlet API does not provide a way to create error page mappings in Java. You can, however, use both a `WebApplicationInitializer` and a minimal `web.xml`. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/localeresolver.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/localeresolver.adoc index b489b453149b..67ebebd5515c 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/localeresolver.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/localeresolver.adoc @@ -54,44 +54,9 @@ information. This locale resolver inspects a `Cookie` that might exist on the client to see if a `Locale` or `TimeZone` is specified. If so, it uses the specified details. By using the properties of this locale resolver, you can specify the name of the cookie as well as the -maximum age. The following example defines a `CookieLocaleResolver`: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- - -The following table describes the properties `CookieLocaleResolver`: - -[[mvc-cookie-locale-resolver-props-tbl]] -.CookieLocaleResolver properties -[cols="1,1,4"] -|=== -| Property | Default | Description - -| `cookieName` -| class name + LOCALE -| The name of the cookie - -| `cookieMaxAge` -| Servlet container default -| The maximum time a cookie persists on the client. If `-1` is specified, the - cookie will not be persisted. It is available only until the client shuts down - the browser. - -| `cookiePath` -| / -| Limits the visibility of the cookie to a certain part of your site. When `cookiePath` is - specified, the cookie is visible only to that path and the paths below it. -|=== +maximum age. The following example defines a `CookieLocaleResolver` bean: +include-code::./WebConfiguration[tag=snippet,indent=0] [[mvc-localeresolver-session]] == Session Resolver @@ -115,31 +80,7 @@ You can enable changing of locales by adding the `LocaleChangeInterceptor` to on accordingly, calling the `setLocale` method on the `LocaleResolver` in the dispatcher's application context. The next example shows that calls to all `{asterisk}.view` resources that contain a parameter named `siteLanguage` now changes the locale. So, for example, -a request for the URL, `https://www.sf.net/home.view?siteLanguage=nl`, changes the site +a request for the URL `https://domain.com/home.view?siteLanguage=nl` changes the site language to Dutch. The following example shows how to intercept the locale: -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - - - - - - - - - /**/*.view=someController - - ----- - - - +include-code::./WebConfiguration[tag=snippet,indent=0,chomp=-tags] 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..1c3523c5f958 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 @@ -25,62 +25,7 @@ through the `enableLoggingRequestDetails` property on `DispatcherServlet`. The following example shows how to do so by using Java configuration: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- -public class MyInitializer - extends AbstractAnnotationConfigDispatcherServletInitializer { - - @Override - protected Class[] getRootConfigClasses() { - return ... ; - } - - @Override - protected Class[] getServletConfigClasses() { - return ... ; - } - - @Override - protected String[] getServletMappings() { - return ... ; - } - - @Override - protected void customizeRegistration(ServletRegistration.Dynamic registration) { - registration.setInitParameter("enableLoggingRequestDetails", "true"); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { - - override fun getRootConfigClasses(): Array>? { - return ... - } - - override fun getServletConfigClasses(): Array>? { - return ... - } - - override fun getServletMappings(): Array { - return ... - } - - override fun customizeRegistration(registration: ServletRegistration.Dynamic) { - registration.setInitParameter("enableLoggingRequestDetails", "true") - } - } ----- -====== +include-code::./MyInitializer[tag=snippet,indent=0] 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 d23200eb2a91..23febfb6ba61 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 @@ -6,8 +6,6 @@ `MultipartResolver` from the `org.springframework.web.multipart` package is a strategy for parsing multipart requests including file uploads. There is a container-based `StandardServletMultipartResolver` implementation for Servlet multipart request parsing. -Note that the outdated `CommonsMultipartResolver` based on Apache Commons FileUpload is -not available anymore, as of Spring Framework 6.0 with its new Servlet 5.0+ baseline. To enable multipart handling, you need to declare a `MultipartResolver` bean in your `DispatcherServlet` Spring configuration with a name of `multipartResolver`. @@ -28,43 +26,7 @@ To do so: The following example shows how to set a `MultipartConfigElement` on the Servlet registration: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - - // ... - - @Override - protected void customizeRegistration(ServletRegistration.Dynamic registration) { - - // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold - registration.setMultipartConfig(new MultipartConfigElement("/tmp")); - } - - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { - - // ... - - override fun customizeRegistration(registration: ServletRegistration.Dynamic) { - - // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold - registration.setMultipartConfig(MultipartConfigElement("/tmp")) - } - - } ----- -====== +include-code::./AppInitializer[tag=snippet,indent=0] Once the Servlet multipart configuration is in place, you can add a bean of type `StandardServletMultipartResolver` with a name of `multipartResolver`. 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 27daeeb49137..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 @@ -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. |=== 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..85da18dc57a8 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 @@ -3,12 +3,11 @@ [.small]#xref:web/webflux/uri-building.adoc[See equivalent in the Reactive stack]# -This section describes various options available in the Spring Framework to work with URI's. +This section describes various options available in the Spring Framework to work with URIs. include::partial$web/web-uris.adoc[leveloffset=+1] - [[mvc-servleturicomponentsbuilder]] == Relative Servlet Requests @@ -19,7 +18,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 +31,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val request: HttpServletRequest = ... @@ -50,7 +49,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 +63,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val request: HttpServletRequest = ... @@ -84,7 +83,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 +97,7 @@ Java:: Kotlin:: + -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim,quotes"] ---- val request: HttpServletRequest = ... @@ -113,9 +112,8 @@ Kotlin:: NOTE: As of 5.1, `ServletUriComponentsBuilder` ignores information from the `Forwarded` and `X-Forwarded-*` headers, which specify the client-originated address. Consider using the -xref:web/webmvc/filters.adoc#filters-forwarded-headers[`ForwardedHeaderFilter`] to extract and use or to discard -such headers. - +xref:web/webmvc/filters.adoc#filters-forwarded-headers[`ForwardedHeaderFilter`] +to extract and use or to discard such headers. [[mvc-links-to-controllers]] @@ -128,7 +126,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 +141,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 +161,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 +171,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 +195,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 +205,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 +238,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 +249,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) @@ -263,9 +261,8 @@ Kotlin:: NOTE: As of 5.1, `MvcUriComponentsBuilder` ignores information from the `Forwarded` and `X-Forwarded-*` headers, which specify the client-originated address. Consider using the -xref:web/webmvc/filters.adoc#filters-forwarded-headers[ForwardedHeaderFilter] to extract and use or to discard -such headers. - +xref:web/webmvc/filters.adoc#filters-forwarded-headers[ForwardedHeaderFilter] to extract +and use or to discard such headers. [[mvc-links-to-controllers-from-views]] @@ -280,7 +277,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 +289,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 { @@ -322,7 +319,3 @@ capital letters of the class and the method name (for example, the `getThing` me `ThingController` becomes "TC#getThing"). If there is a name clash, you can use `@RequestMapping(name="..")` to assign an explicit name or implement your own `HandlerMethodMappingNamingStrategy`. - - - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket.adoc b/framework-docs/modules/ROOT/pages/web/websocket.adoc index 726c9c2de3ed..9f623514262a 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 @@ -8,5 +9,3 @@ messaging that includes raw WebSocket interactions, WebSocket emulation through publish-subscribe messaging through STOMP as a sub-protocol over WebSocket. include::partial$web/websocket-intro.adoc[leveloffset=+1] - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc b/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc index cafe41a81415..5c39cc7ff489 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/fallback.adoc @@ -13,7 +13,6 @@ On the Servlet stack, the Spring Framework provides both server (and also client for the SockJS protocol. - [[websocket-fallback-sockjs-overview]] == Overview @@ -78,7 +77,6 @@ For even more detail, see the SockJS protocol https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html[narrated test]. - [[websocket-fallback-sockjs-enable]] == Enabling SockJS @@ -101,7 +99,6 @@ transport types supported by browser. The client also provides several configuration options -- for example, to specify which transports to include. - [[websocket-fallback-xhr-vs-iframe]] == IE 8 and 9 @@ -155,26 +152,9 @@ from the iframe. By default, the iframe is set to download the SockJS client from a CDN location. It is a good idea to configure this option to use a URL from the same origin as the application. -The following example shows how to do so in Java configuration: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @EnableWebSocketMessageBroker - public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/portfolio").withSockJS() - .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js"); - } - - // ... - - } ----- +The following example shows how to configure it: -The XML namespace provides a similar option through the `` element. +include-code::./WebSocketConfiguration[tag=snippet,indent=0] 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 @@ -182,7 +162,6 @@ be cached. For details on how to enable it see the {sockjs-client}[SockJS client] page. - [[websocket-fallback-sockjs-heartbeat]] == Heartbeats @@ -198,11 +177,10 @@ 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. - [[websocket-fallback-sockjs-servlet3-async]] == Client Disconnects @@ -229,15 +207,15 @@ a minimal message by using the dedicated log category, `DISCONNECTED_CLIENT_LOG_ log category to TRACE. - [[websocket-fallback-cors]] == SockJS and CORS -If you allow cross-origin requests (see xref:web/websocket/server.adoc#websocket-server-allowed-origins[Allowed Origins]), the SockJS protocol -uses CORS for cross-domain support in the XHR streaming and polling transports. Therefore, -CORS headers are added automatically, unless the presence of CORS headers in the response -is detected. So, if an application is already configured to provide CORS support (for example, -through a Servlet Filter), Spring's `SockJsService` skips this part. +If you allow cross-origin requests (see +xref:web/websocket/server.adoc#websocket-server-allowed-origins[Allowed Origins]), the SockJS +protocol uses CORS for cross-domain support in the XHR streaming and polling transports. +Therefore, CORS headers are added automatically, unless the presence of CORS headers in the +response is detected. So, if an application is already configured to provide CORS support +(for example, through a Servlet Filter), Spring's `SockJsService` skips this part. It is also possible to disable the addition of these CORS headers by setting the `suppressCors` property in Spring's SockJsService. @@ -257,7 +235,6 @@ Alternatively, if the CORS configuration allows it, consider excluding URLs with SockJS endpoint prefix, thus letting Spring's `SockJsService` handle it. - [[websocket-fallback-sockjs-client]] == `SockJsClient` @@ -280,7 +257,7 @@ An `XhrTransport`, by definition, supports both `xhr-streaming` and `xhr-polling from a client perspective, there is no difference other than in the URL used to connect to the server. At present there are two implementations: -* `RestTemplateXhrTransport` uses Spring's `RestTemplate` for HTTP requests. +* `RestClientXhrTransport` uses Spring's `RestClient` for HTTP requests. * `JettyXhrTransport` uses Jetty's `HttpClient` for HTTP requests. The following example shows how to create a SockJS client and connect to a SockJS endpoint: @@ -289,7 +266,7 @@ The following example shows how to create a SockJS client and connect to a SockJ ---- List transports = new ArrayList<>(2); transports.add(new WebSocketTransport(new StandardWebSocketClient())); - transports.add(new RestTemplateXhrTransport()); + transports.add(new RestClientXhrTransport()); SockJsClient sockJsClient = new SockJsClient(transports); sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs"); @@ -313,27 +290,4 @@ jettyHttpClient.setExecutor(new QueuedThreadPool(1000)); The following example shows the server-side SockJS-related properties (see javadoc for details) that you should also consider customizing: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/sockjs").withSockJS() - .setStreamBytesLimit(512 * 1024) <1> - .setHttpMessageCacheSize(1000) <2> - .setDisconnectDelay(30 * 1000); <3> - } - - // ... - } ----- -<1> Set the `streamBytesLimit` property to 512KB (the default is 128KB -- `128 * 1024`). -<2> Set the `httpMessageCacheSize` property to 1,000 (the default is `100`). -<3> Set the `disconnectDelay` property to 30 property seconds (the default is five seconds --- `5 * 1000`). - - - - +include-code::./WebSocketConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc index 5d5bbf5fa6fa..b17bc2e0bc69 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc @@ -7,7 +7,6 @@ The Spring Framework provides a WebSocket API that you can use to write client- server-side applications that handle WebSocket messages. - [[websocket-server-handler]] == `WebSocketHandler` [.small]#xref:web/webflux-websocket.adoc#webflux-websocket-server-handler[See equivalent in the Reactive stack]# @@ -29,14 +28,13 @@ 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 {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 {spring-framework-api}/web/socket/handler/ConcurrentWebSocketSessionDecorator.html[`ConcurrentWebSocketSessionDecorator`]. - [[websocket-server-handshake]] == WebSocket Handshake [.small]#xref:web/webflux-websocket.adoc#webflux-websocket-server-handshake[See equivalent in the Reactive stack]# @@ -67,7 +65,6 @@ exceptions that arise from any `WebSocketHandler` method and closes the WebSocke session with status `1011`, which indicates a server error. - [[websocket-server-deployment]] == Deployment @@ -85,10 +82,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 @@ -132,7 +126,6 @@ Java initialization API. The following example shows how to do so: ---- - [[websocket-server-runtime-configuration]] == Configuring the Server [.small]#xref:web/webflux-websocket.adoc#webflux-websocket-server-config[See equivalent in the Reactive stack]# @@ -158,7 +151,6 @@ xref:web/websocket/stomp/server-config.adoc[STOMP WebSocket transport] properties. - [[websocket-server-allowed-origins]] == Allowed Origins [.small]#xref:web/webflux-websocket.adoc#webflux-websocket-server-cors[See equivalent in the Reactive stack]# diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp.adoc index 405d956c8a13..aa8c4ac59753 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp.adoc @@ -8,6 +8,3 @@ sub-protocol (that is, a higher-level messaging protocol) to use on top of WebSo define what kind of messages each can send, what the format is, the content of each message, and so on. The use of a sub-protocol is optional but, either way, the client and the server need to agree on some protocol that defines message content. - - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/application-context-events.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/application-context-events.adoc index 7d59414d2ccd..5fd70abac99d 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/application-context-events.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/application-context-events.adoc @@ -35,6 +35,3 @@ NOTE: When you use a full-featured broker, the STOMP "`broker relay`" automatica however, are not automatically reconnected. Assuming heartbeats are enabled, the client typically notices the broker is not responding within 10 seconds. Clients need to implement their own reconnecting logic. - - - 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 b65811e74b8a..f88920cb87d4 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 @@ -47,6 +47,3 @@ you need to ensure that the authentication `ChannelInterceptor` config is ordere ahead of Spring Security's. This is best done by declaring the custom interceptor in its own implementation of `WebSocketMessageBrokerConfigurer` that is marked with `@Order(Ordered.HIGHEST_PRECEDENCE + 99)`. - - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication.adoc index b8dafd67d43d..a2bc8d09db75 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authentication.adoc @@ -29,6 +29,3 @@ Those were originally designed for and are needed for STOMP over TCP. However, f over WebSocket, by default, Spring ignores authentication headers at the STOMP protocol level, and assumes that the user is already authenticated at the HTTP transport level. The expectation is that the WebSocket or SockJS session contain the authenticated user. - - - 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 95af447e597e..a8a3bc520a05 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/authorization.adoc @@ -8,6 +8,3 @@ that uses a `ChannelInterceptor` to authorize messages based on the user header Also, Spring Session provides {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/benefits.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/benefits.adoc index 31e3e7f32240..3550cc143375 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/benefits.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/benefits.adoc @@ -16,6 +16,3 @@ manage subscriptions and broadcast messages. routed to them based on the STOMP destination header versus handling raw WebSocket messages with a single `WebSocketHandler` for a given connection. * You can use Spring Security to secure messages based on STOMP destinations and message types. - - - 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 ba205223a86b..9de2b02e429a 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc @@ -119,6 +119,3 @@ messages. When an inbound STOMP message size exceeds the configured limit, a 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 cc5df948025f..c1706a321d38 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 @@ -72,7 +72,7 @@ such as https://github.com/stomp-js/stompjs[`stomp-js/stompjs`] and others split 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 +Spring's STOMP-over-WebSocket support does this, so applications can configure the maximum size for STOMP messages irrespective of WebSocket server-specific message sizes. Keep in mind that the WebSocket message size is automatically adjusted, if necessary, to ensure they can carry 16K WebSocket messages at a @@ -88,6 +88,3 @@ However, when you use a full-featured broker (such as RabbitMQ), each applicatio instance connects to the broker, and messages broadcast from one application instance can be broadcast through the broker to WebSocket clients connected through any other application instances. - - - 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 0c81e2c2b861..51de770cf82c 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 @@ -24,6 +24,3 @@ the broker you use to see what conventions it supports for the destination heade The "`simple broker`", on the other hand, does rely on the configured `PathMatcher`, so, if you switch the separator, that change also applies to the broker and the way the broker matches destinations from a message to patterns in subscriptions. - - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc index 4301ba970868..021093ab65bc 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc @@ -22,11 +22,11 @@ The following example code is based on it: [source,javascript,indent=0,subs="verbatim,quotes"] ---- const stompClient = new StompJs.Client({ - brokerURL: 'ws://domain.com/portfolio', - onConnect: () => { - // ... - } - }); + brokerURL: 'ws://domain.com/portfolio', + onConnect: () => { + // ... + } + }); ---- Alternatively, if you connect through SockJS, you can enable the @@ -46,6 +46,3 @@ For more example code see: 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-annotations.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-annotations.adoc index 7479fd6dc979..db856e597c07 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-annotations.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-annotations.adoc @@ -180,6 +180,3 @@ Typically, `@MessageExceptionHandler` methods apply within the `@Controller` cla more globally (across controllers), you can declare them in a class marked with `@ControllerAdvice`. This is comparable to the xref:web/webmvc/mvc-controller/ann-advice.adoc[similar support] available in Spring MVC. - - - 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 f13d532367fe..39abc5839945 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 @@ -43,6 +43,3 @@ The value of this property is set as the `host` header of every `CONNECT` frame and can be useful (for example, in a cloud environment where the actual host to which the TCP connection is established differs from the host that provides the cloud-based STOMP service). - - - 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 fd0ddcec2267..8150421b59af 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 @@ -33,6 +33,3 @@ business services, and others) can also send messages to the broker relay, as de in xref:web/websocket/stomp/handle-send.adoc[Sending Messages], to broadcast messages to subscribed WebSocket clients. In effect, the broker relay enables robust and scalable message broadcasting. - - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-send.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-send.adoc index e193522ab0c2..0af57f190373 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-send.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/handle-send.adoc @@ -30,6 +30,3 @@ type, as the following example shows: However, you can also qualify it by its name (`brokerMessagingTemplate`), if another bean of the same type exists. - - - 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 51ae36f7713a..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,7 +13,7 @@ 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: 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 9bdab9835166..1f7c794edb38 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/interceptors.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/interceptors.adoc @@ -24,6 +24,3 @@ can be from the client or it can also be automatically generated when the WebSocket session is closed. In some cases, an interceptor may intercept this message more than once for each session. Components should be idempotent with regard to multiple disconnect events. - - - 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 aee0cd9adc8b..2b4cb25c87bc 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 @@ -85,6 +85,3 @@ and sent on the WebSocket connection. The next section provides more details on annotated methods, including the kinds of arguments and return values that are supported. - - - 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 a5d5f82f3ad6..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 @@ -19,6 +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: +To enable ordered receiving, set the `setPreserveReceiveOrder` flag as follows: include-code::./ReceiveOrderWebSocketConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/overview.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/overview.adoc index cf94589ba1d6..40bd603dd031 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/overview.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/overview.adoc @@ -59,7 +59,7 @@ destination:/queue/trade content-type:application/json content-length:44 -{"action":"BUY","ticker":"MMM","shares",44}^@ +{"action":"BUY","ticker":"MMM","shares":44}^@ ---- After the execution, the server can @@ -92,6 +92,3 @@ client subscription. The preceding overview is intended to provide the most basic understanding of the STOMP protocol. We recommended reviewing the protocol https://stomp.github.io/stomp-specification-1.2.html[specification] in full. - - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/scope.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/scope.adoc index b4300990abe6..0be9eecb94a4 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/scope.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/scope.adoc @@ -1,8 +1,8 @@ [[websocket-stomp-websocket-scope]] = WebSocket Scope -Each WebSocket session has a map of attributes. The map is attached as a header to -inbound client messages and may be accessed from a controller method, as the following example shows: +Each WebSocket session has a map of attributes. The map is attached as a header to inbound +client messages and may be accessed from a controller method, as the following example shows: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -20,13 +20,13 @@ public class MyController { You can declare a Spring-managed bean in the `websocket` scope. You can inject WebSocket-scoped beans into controllers and any channel interceptors registered on the `clientInboundChannel`. Those are typically singletons and live -longer than any individual WebSocket session. Therefore, you need to use a -scope proxy mode for WebSocket-scoped beans, as the following example shows: +longer than any individual WebSocket session. Therefore, you need to use +WebSocket-scoped beans in proxy mode, conveniently defined with `@WebSocketScope`: [source,java,indent=0,subs="verbatim,quotes"] ---- @Component - @Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS) + @WebSocketScope public class MyBean { @PostConstruct @@ -64,6 +64,3 @@ time it is accessed from the controller and stores the instance in the WebSocket session attributes. The same instance is subsequently returned until the session ends. WebSocket-scoped beans have all Spring lifecycle methods invoked, as shown in the preceding examples. - - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/stats.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/stats.adoc index 107f353fd425..eb8e4f69b935 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/stats.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/stats.adoc @@ -54,6 +54,3 @@ Client Outbound Channel:: Statistics from the thread pool that backs the `client SockJS Task Scheduler:: Statistics from the thread pool of the SockJS task scheduler that is used to send heartbeats. Note that, when heartbeats are negotiated on the STOMP level, the SockJS heartbeats are disabled. - - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/user-destination.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/user-destination.adoc index 84fef9ffb797..9d22069b27bd 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/user-destination.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/user-destination.adoc @@ -112,6 +112,3 @@ destination to broadcast unresolved messages so that other servers have a chance This can be done through the `userDestinationBroadcast` property of the `MessageBrokerRegistry` in Java configuration and the `user-destination-broadcast` attribute of the `message-broker` element in XML. - - - diff --git a/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc index 9ba46a5f832a..1ba007dec0fa 100644 --- a/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc +++ b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc @@ -1,18 +1,30 @@ -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. +As a request goes through a chain of proxies, request details such as the scheme, host, +port, remote address, and local address change. Proxies can insert headers that keep track of +the hops, and that can help to restore the request from the original client's perspective. +This allows an application to create self-reference links for external clients. -{rfc-site}/rfc7239[RFC 7239] defines the `Forwarded` HTTP header -that proxies can use to provide information about the original request. +There are two alternatives for headers that proxies can use: +- {rfc-site}/rfc7239[RFC 7239] defines the `"Forwarded"` HTTP header, a single header with +individual attributes for each component in the chain of proxied requests with the +following syntax. +- `"X-Forwarded-"` prefixed headers are the original approach that predates the standard +and uses a separate header for each request component. +The Spring Framework supports both approaches. Most proxies today support the original +`"X-Forwarded"` headers only as a de facto standard. + +WARNING: For maximum security, a proxy at the edge of trust must be configured to reset both +the standard `"Forwarded"` and `"X-Forwarded-"` headers regardless of which ones are chosen +for use. Likewise, when configuring forwarded header handling in Spring, you need to indicate +which type of headers to use. More on security considerations later in this section. -[[forwarded-headers-non-standard]] -=== Non-standard Headers -There are other non-standard headers, too, including `X-Forwarded-Host`, `X-Forwarded-Port`, -`X-Forwarded-Proto`, `X-Forwarded-Ssl`, and `X-Forwarded-Prefix`. +[[forwarded-headers-non-standard]] +=== X-Forwarded Headers + +This section describes supported `"X-Forwarded"` headers. [[x-forwarded-host]] ==== X-Forwarded-Host @@ -23,7 +35,6 @@ downstream server. For example, if a request of `https://example.com/resource` i a proxy which forwards the request to `http://localhost:8080/resource`, then a header of `X-Forwarded-Host: example.com` can be sent to inform the server that the original host was `example.com`. - [[x-forwarded-port]] ==== X-Forwarded-Port @@ -33,27 +44,24 @@ communicate the original port to a downstream server. For example, if a request `http://localhost:8080/resource`, then a header of `X-Forwarded-Port: 443` can be sent to inform the server that the original port was `443`. - [[x-forwarded-proto]] ==== 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 / http) to a downstream server. For example, if a request of `https://example.com/resource` is sent to a proxy which forwards the request to `http://localhost:8080/resource`, then a header of `X-Forwarded-Proto: https` can be sent to inform the server that the original protocol was `https`. - [[x-forwarded-ssl]] ==== 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`. - [[x-forwarded-prefix]] ==== X-Forwarded-Prefix @@ -103,7 +111,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) ==== @@ -119,4 +127,13 @@ https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path} In this case, the proxy has a prefix of `/api/app1` and the server has a prefix of `/app1`. The proxy can send `X-Forwarded-Prefix: /api/app1` to have the original prefix -`/api/app1` override the server prefix `/app1`. \ No newline at end of file +`/api/app1` override the server prefix `/app1`. + +[[x-forwarded-for]] +==== X-Forwarded-For + +https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For[`X-Forwarded-For:
`] +is a de-facto standard header that is used to communicate the original `InetSocketAddress` of the client to a +downstream server. For example, if a request is sent by a client at `[fd00:fefe:1::4]` to a proxy at +`192.168.0.1`, the "remote address" information contained in the HTTP request will reflect the actual address of the +client, not the proxy. diff --git a/framework-docs/modules/ROOT/partials/web/uri-patterns.adoc b/framework-docs/modules/ROOT/partials/web/uri-patterns.adoc new file mode 100644 index 000000000000..494c50a6c0a1 --- /dev/null +++ b/framework-docs/modules/ROOT/partials/web/uri-patterns.adoc @@ -0,0 +1,51 @@ +[cols="2,3,5"] +|=== +|Pattern |Description |Example + +| `spring` +| Literal pattern +| `+"/spring"+` matches `+"/spring"+` + +| `+?+` +| Matches one character +| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+` + +| `+*+` +| Matches zero or more characters within a path segment +| `+"/resources/*.png"+` matches `+"/resources/file.png"+` + +`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`. + +`+"/projects/*"+` matches `+"/projects/spring"+` but does not match `+"/projects"+` as the path segment is not present. + +| `+**+` +| Matches zero or more path segments +| `+"/resources/**"+` matches `+"/resources"+`, `+"/resources/file.png"+` and `+"/resources/images/file.png"+` + +`+"/**/info"+` matches `+"/info"+`, `+"/spring/info"+` and `+"/spring/framework/info"+` + +`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path. + +`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern. + +| `+{name}+` +| Similar to `+*+`, but also captures the path segment as a variable named "name" +| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+` + +`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment. + +| `{name:[a-z]+}` +| Matches the regexp `"[a-z]+"` as a path variable named "name" +| `"/projects/{project:[a-z]+}/versions"` matches `"/projects/spring/versions"` but not `"/projects/spring1/versions"` + +| `+{*path}+` +| Similar to `+**+`, but also captures the path segments as a variable named "path" +| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+` + +`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+` + +`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path. + +`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern. + +|=== \ No newline at end of file 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..007fdcf69bb7 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 @@ -1,91 +1,53 @@ -xref:core/validation/beans-beans.adoc#beans-binding[Data binding] for web requests involves -binding request parameters to a model object. By default, request parameters can be bound -to any public property of the model object, which means malicious clients can provide -extra values for properties that exist in the model object graph, but are not expected to -be set. This is why model object design requires careful consideration. +Data binding involves binding untrusted input onto application objects. +For security reasons, it's crucial to ensure that input is properly constrained to expected fields only. +This section provides guidance for safe binding. -TIP: The model object, and its nested object graph is also sometimes referred to as a -_command object_, _form-backing object_, or _POJO_ (Plain Old Java Object). +First, prefer **immutable object design** for web binding purposes. +It is safe because a constructor naturally constrains binding to expected inputs. +You can use a Java record or a class with a primary constructor, and either can have further nested objects. +See xref:core/validation/data-binding.adoc#data-binding-constructor-binding[Constructor Binding] for details. -A good practice is to use a _dedicated model object_ rather than exposing your domain -model such as JPA or Hibernate entities for web data binding. For example, on a form to -change an email address, create a `ChangeEmailForm` model object that declares only -the properties required for the input: +Another option for safe binding is to use **dedicated objects** designed for the expected input. +Such objects, even if mutable, are safe because they constrain binding to the expected inputs. -[source,java,indent=0,subs="verbatim,quotes"] ----- - public class ChangeEmailForm { - - private String oldEmailAddress; - private String newEmailAddress; - - public void setOldEmailAddress(String oldEmailAddress) { - this.oldEmailAddress = oldEmailAddress; - } - - public String getOldEmailAddress() { - return this.oldEmailAddress; - } - - public void setNewEmailAddress(String newEmailAddress) { - this.newEmailAddress = newEmailAddress; - } - - public String getNewEmailAddress() { - return this.newEmailAddress; - } - - } ----- - -Another good practice is to apply -xref:core/validation/beans-beans.adoc#beans-constructor-binding[constructor binding], -which uses only the request parameters it needs for constructor arguments, and any other -input is ignored. This is in contrast to property binding which by default binds every -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 -sensitive) on `WebDataBinder` in order to prevent unexpected properties from being set. +Domain objects such as JPA or Hibernate entities are generally not safe for web binding +as they likely contain more properties than the expected inputs. +For such cases, it's crucial to declare the properties to expose for binding. For example: [source,java,indent=0,subs="verbatim,quotes"] ---- @Controller - public class ChangeEmailController { + public class PersonController { @InitBinder void initBinder(WebDataBinder binder) { - binder.setAllowedFields("oldEmailAddress", "newEmailAddress"); + // See Javadoc for supported pattern syntax + binder.setAllowedFields("firstName", "lastName", "*Address"); } - - // @RequestMapping methods, etc. - } ---- -You can also register `disallowedFields` patterns (case insensitive). However, -"allowed" configuration is preferred over "disallowed" as it is more explicit and less -prone to mistakes. +NOTE: The `disallowedFields` property has been +https://github.com/spring-projects/spring-framework/issues/36802[deprecated in Spring Framework 7.1] +because it is fragile and easy to get out of sync with the actual properties over time. -By default, constructor and property binding are both used. If you want to use -constructor binding only, you can set the `declarativeBinding` flag on `WebDataBinder` -through an `@InitBinder` method either locally within a controller or globally through an -`@ControllerAdvice`. Turning this flag on ensures that only constructor binding is used -and that property binding is not used unless `allowedFields` patterns are configured. -For example: +By default, `DataBinder` applies both constructor and setter binding. +This is fine with immutable objects and dedicated objects, but for domain objects, you must +remember to set `allowedFields`. To ensure data binding is only used in declarative style where +expected inputs are explicitly declared, you can set `declarativeBinding` on `DataBinder`. +That applies constructor binding always, and setter binding conditionally if `allowedFields` is set. +The following shows how to set this flag globally, or +you can also narrow it through attributes on `ControllerAdvice`: [source,java,indent=0,subs="verbatim,quotes"] ---- - @Controller - public class MyController { + @ControllerAdvice + public class ControllerConfig { @InitBinder void initBinder(WebDataBinder binder) { binder.setDeclarativeBinding(true); } - - // @RequestMapping methods, etc. - } ---- diff --git a/framework-docs/modules/ROOT/partials/web/web-uris.adoc b/framework-docs/modules/ROOT/partials/web/web-uris.adoc index a8bde952e68e..4952f05dd5a7 100644 --- a/framework-docs/modules/ROOT/partials/web/web-uris.adoc +++ b/framework-docs/modules/ROOT/partials/web/web-uris.adoc @@ -2,13 +2,13 @@ = UriComponents [.small]#Spring MVC and Spring WebFlux# -`UriComponentsBuilder` helps to build URI's from URI templates with variables, as the following example shows: +`UriComponentsBuilder` helps to build URIs from URI templates with variables, as the following example shows: [tabs] ====== 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}") @@ -128,7 +128,7 @@ Kotlin:: = UriBuilder [.small]#Spring MVC and Spring WebFlux# -<> implements `UriBuilder`. You can create a +<> implements `UriBuilder`. You can create a `UriBuilder`, in turn, with a `UriBuilderFactory`. Together, `UriBuilderFactory` and `UriBuilder` provide a pluggable mechanism to build URIs from URI templates, based on shared configuration, such as a base URL, encoding preferences, and other details. @@ -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 URLs. 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 URLs 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 URLs more leniently, and to align with the way +browsers parse URIs, 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: -* {spring-framework-api}/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. -* {spring-framework-api}/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,35 +370,35 @@ 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) + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl); factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES); - // Customize the RestTemplate.. + // Customize the RestTemplate. RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(factory); - // Customize the WebClient.. + // Customize the WebClient. WebClient client = WebClient.builder().uriBuilderFactory(factory).build(); ---- 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 { encodingMode = EncodingMode.TEMPLATE_AND_VALUES } - // Customize the RestTemplate.. + // Customize the RestTemplate. val restTemplate = RestTemplate().apply { uriTemplateHandler = factory } - // Customize the WebClient.. + // Customize the WebClient. val client = WebClient.builder().uriBuilderFactory(factory).build() ---- ====== @@ -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 8598a214be91..e2a18e4c2f67 100644 --- a/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc +++ b/framework-docs/modules/ROOT/partials/web/websocket-intro.adoc @@ -46,14 +46,12 @@ 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. - - [[http-versus-websocket]] == HTTP Versus WebSocket @@ -78,8 +76,6 @@ WebSocket clients and servers can negotiate the use of a higher-level, messaging In the absence of that, they need to come up with their own conventions. - - [[when-to-use-websockets]] == When to Use WebSockets diff --git a/framework-docs/package.json b/framework-docs/package.json index c3570e2f8a62..09bac4fdbd35 100644 --- a/framework-docs/package.json +++ b/framework-docs/package.json @@ -1,10 +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", + "antora": "3.2.0-alpha.12", + "@antora/atlas-extension": "1.0.0-alpha.5", + "@antora/collector-extension": "1.0.3", "@asciidoctor/tabs": "1.0.0-beta.6", - "@springio/antora-extensions": "1.11.1", - "@springio/asciidoctor-extensions": "1.0.0-alpha.10" + "@springio/antora-extensions": "1.14.12", + "fast-xml-parser": "5.7.0", + "@springio/asciidoctor-extensions": "1.0.0-alpha.18" } } diff --git a/framework-docs/src/docs/dist/license.txt b/framework-docs/src/docs/dist/license.txt index 89cf3d232fa7..cf508c867633 100644 --- a/framework-docs/src/docs/dist/license.txt +++ b/framework-docs/src/docs/dist/license.txt @@ -200,6 +200,7 @@ See the License for the specific language governing permissions and limitations under the License. + ======================================================================= SPRING FRAMEWORK ${version} SUBCOMPONENTS: @@ -212,7 +213,7 @@ code for these subcomponents is subject to the terms and conditions of the following licenses. ->>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): +>>> ASM 9.9.1 (org.ow2.asm:asm:9.9.1): Copyright (c) 2000-2011 INRIA, France Telecom All rights reserved. @@ -249,32 +250,28 @@ Copyright (c) 1999-2009, OW2 Consortium >>> CGLIB 3.3 (cglib:cglib:3.3): -Per the LICENSE file in the CGLIB JAR distribution downloaded from -https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, -CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which -is included above. +Per the LICENSE file in the CGLIB distribution, CGLIB 3.3 is licensed +under the Apache License, version 2.0, the text of which is included above. ->>> JavaPoet 1.13.0 (com.squareup:javapoet:1.13.0): +>>> JavaPoet 0.10.0 (com.palantir.javapoet:javapoet:0.10.0): -Per the LICENSE file in the JavaPoet JAR distribution downloaded from -https://github.com/square/javapoet/archive/refs/tags/javapoet-1.13.0.zip, -JavaPoet 1.13.0 is licensed under the Apache License, version 2.0, the text of -which is included above. +Per the LICENSE file in the JavaPoet distribution, JavaPoet 0.10.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.5 (org.objenesis:objenesis:3.5): -Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.2 is licensed under the +Per the LICENSE file in the Objenesis distribution downloaded from +http://objenesis.org/download.html, Objenesis 3.5 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 +Per the NOTICE file in the Objenesis distribution downloaded from http://objenesis.org/download.html and corresponding to section 4d of the Apache License, Version 2.0, in this case for Objenesis: Objenesis -Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita +Copyright 2006-2026 Joe Walnes, Henri Tremblay, Leonardo Mesquita =============================================================================== 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 index 4f1456b36e19..3ab875d32acc 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 0a51c937e296..9ff2e12705ed 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a7b6bb6496d2..e52c059baf23 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a45001fdadab..8d5178b43b80 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 522acd7ea6d9..8c8bdca31042 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 337fe1265143..c69d9b2075f1 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index abba61921144..f39bf438eaa9 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a760f744918f..373f655869cc 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 394f21b43a6a..7424cef93458 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 70636063a604..5d2e05c64ace 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 84eb3a7f5a8f..a3373e79edc1 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a56be401eba5..17e2b3c4ee0d 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/importruntimehints/SpellCheckService.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/importruntimehints/SpellCheckService.java index 88fd79becf0a..1bc5038e65e2 100644 --- a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/importruntimehints/SpellCheckService.java +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/importruntimehints/SpellCheckService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..42ee3426e2c5 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/reflective/MyConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..b7f0278b9ed9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/registerreflection/MyConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..f69235815a6f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/registerreflection/OrderService.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 2772146e71ea..83d91da8b6a3 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..909a7a14cb1f 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-present the original author 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 cd4d87731b60..0921e9073563 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/refresh/AotProcessingSample.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/refresh/AotProcessingSample.java index 0c153fc8975e..a811321301ea 100644 --- a/framework-docs/src/main/java/org/springframework/docs/core/aot/refresh/AotProcessingSample.java +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/refresh/AotProcessingSample.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 3bb14d4e7220..1e94812a52cf 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index c1f1d3cc0548..e4f556e15cab 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 1d08cf43247a..61598e0acdf6 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 7f2685ba14e1..4aee52fa24fe 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.java b/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.java new file mode 100644 index 000000000000..22fb1c08c4b3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.java.beansjavaprogrammaticregistration; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.core.env.Environment; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; + +// tag::snippet[] +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!"))); + } + registry.registerBean(MyRepository.class); + registry.registerBean(RouterFunction.class, spec -> + spec.supplier(context -> router(context.bean(MyRepository.class)))); + } + + RouterFunction router(MyRepository myRepository) { + return RouterFunctions.route() + // ... + .build(); + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.java new file mode 100644 index 000000000000..8593224eacee --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.java.beansjavaprogrammaticregistration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +// tag::snippet[] +@Configuration +@Import(MyBeanRegistrar.class) +class MyConfiguration { +} +// 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 index cadf841666ce..49a8d78b47a6 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 6c795eb47517..5309e4b73eb4 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index aee9cba8d7f5..d2cb20695165 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 8ac7b7d9f3ac..d5eb6def42be 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 94b519cc56b9..50099e967a45 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 1bcfad4b1d69..a2fa3d62b4e8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 0fb0da1322cd..30af7a037e73 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 9ce82ee6b5b4..34a765df47e1 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/core/expressions/languageref/expressionsoperatorsoverloaded/ListConcatenation.java b/framework-docs/src/main/java/org/springframework/docs/core/expressions/languageref/expressionsoperatorsoverloaded/ListConcatenation.java new file mode 100644 index 000000000000..c17c3a82ef39 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/expressions/languageref/expressionsoperatorsoverloaded/ListConcatenation.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.languageref.expressionsoperatorsoverloaded; + +import org.springframework.expression.Operation; +import org.springframework.expression.OperatorOverloader; + +import java.util.ArrayList; +import java.util.List; + +public 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({"rawtypes", "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)); + } + +} 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 index 7f151848c9c7..7d2357bda433 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..404adaeabe2c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..07686dc1c52e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..d32d621b2246 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..18cc43559133 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..a9c7ac75ebd8 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 2fd9109fc793..4005de91eb4b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 45e842fdbf46..fc2e3ae7b289 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 0783c9228907..0b1936966532 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a48a3c6e800b..c7c43ab5a7c6 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 9762d8ae3804..7098c43addd5 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a0eb1fe7f881..33525eed4c6f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 36ab9956b738..a74296013f40 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 1597edd00af1..afb3d8040305 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/AppConfig.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/AppConfig.java new file mode 100644 index 000000000000..8d5cebe5efa4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/AppConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.transaction.declarative.transactiondeclarativeannotations; + +import javax.sql.DataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +// tag::snippet[] +@Configuration +@EnableTransactionManagement +public class AppConfig { + + @Bean + public FooService fooService() { + return new DefaultFooService(); + } + + @Bean + public PlatformTransactionManager txManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/DefaultFooService.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/DefaultFooService.java new file mode 100644 index 000000000000..8ccceaf3b476 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/DefaultFooService.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.transaction.declarative.transactiondeclarativeannotations; + +public class DefaultFooService implements FooService { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/FooService.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/FooService.java new file mode 100644 index 000000000000..95841a510c9a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/FooService.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.transaction.declarative.transactiondeclarativeannotations; + +public interface FooService { +} 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 index 53411fa7138e..2801418f9a85 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ // tag::snippet[] @Configuration @EnableCaching -public class CacheConfiguration { +class CacheConfiguration { @Bean CacheManager cacheManager() { 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 index 6618ee3353ee..c927ff614944 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class CacheConfiguration { +class CacheConfiguration { // tag::snippet[] @Bean 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 index db87fcc981c7..cfc6dc9e79c2 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class CustomCacheConfiguration { +class CustomCacheConfiguration { // tag::snippet[] @Bean 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 index c8fae52e56b1..7d826e1cf6d7 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class CacheConfiguration { +class CacheConfiguration { // tag::snippet[] @Bean 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 index 1c83d6c446ed..0dafaaa85325 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class CacheConfiguration { +class CacheConfiguration { // tag::snippet[] @Bean 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 index 6944e7b785f3..706da256be61 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class CacheConfiguration { +class CacheConfiguration { private CacheManager jdkCache() { return null; 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 index 932988ce632a..83376eb0bff3 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index dc183df8eb3a..1945ca8eb1b9 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a95e1c8aafe3..4edfb1742650 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index d535f8cb0811..9953a2b09e58 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 6f9fb309ae25..1ce3ef673718 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index acad7cbcc248..66e25a17079e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index bfa2e6168455..f418cd7e20b3 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 00d32876ac26..a7e1875f9547 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 6bb0bf75cd1e..ab959256a809 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index f15b002ff2f1..7d96717473bb 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 565e2dffe17f..1ddcf4ec8a90 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ae22b4838fce..27e76bfab641 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ecd0f3007dc8..10670d00cd11 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssending/JmsQueueSender.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssending/JmsQueueSender.java new file mode 100644 index 000000000000..c0427bab7a14 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssending/JmsQueueSender.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.jmssending; + +import jakarta.jms.ConnectionFactory; +import jakarta.jms.JMSException; +import jakarta.jms.Message; +import jakarta.jms.Queue; +import jakarta.jms.Session; + +import org.springframework.jms.core.MessageCreator; +import org.springframework.jms.core.JmsTemplate; + +public class JmsQueueSender { + + private JmsTemplate jmsTemplate; + private Queue queue; + + public void setConnectionFactory(ConnectionFactory cf) { + this.jmsTemplate = new JmsTemplate(cf); + } + + public void setQueue(Queue queue) { + this.queue = queue; + } + + public void simpleSend() { + this.jmsTemplate.send(this.queue, new MessageCreator() { + public Message createMessage(Session session) throws JMSException { + return session.createTextMessage("hello queue world"); + } + }); + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingconversion/JmsSenderWithConversion.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingconversion/JmsSenderWithConversion.java new file mode 100644 index 000000000000..f55446b0ef03 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingconversion/JmsSenderWithConversion.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.jmssendingconversion; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.jms.JMSException; +import jakarta.jms.Message; + +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.core.MessagePostProcessor; + +public class JmsSenderWithConversion { + + private JmsTemplate jmsTemplate; + + public void sendWithConversion() { + Map map = new HashMap<>(); + map.put("Name", "Mark"); + map.put("Age", 47); + jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() { + public Message postProcessMessage(Message message) throws JMSException { + message.setIntProperty("AccountID", 1234); + message.setJMSCorrelationID("123-00001"); + return message; + } + }); + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingjmsclient/JmsClientSample.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingjmsclient/JmsClientSample.java new file mode 100644 index 000000000000..3f7468c839c3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingjmsclient/JmsClientSample.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.jmssendingjmsclient; + +import jakarta.jms.ConnectionFactory; + +import org.springframework.jms.core.JmsClient; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +public class JmsClientSample { + + private final JmsClient jmsClient; + + public JmsClientSample(ConnectionFactory connectionFactory) { + // For custom options, use JmsClient.builder(ConnectionFactory) + this.jmsClient = JmsClient.create(connectionFactory); + } + + public void sendWithConversion() { + this.jmsClient.destination("myQueue") + .withTimeToLive(1000) + .send("myPayload"); // optionally with a headers Map next to the payload + } + + public void sendCustomMessage() { + Message message = MessageBuilder.withPayload("myPayload").build(); // optionally with headers + this.jmsClient.destination("myQueue") + .withTimeToLive(1000) + .send(message); + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingpostprocessor/JmsClientWithPostProcessor.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingpostprocessor/JmsClientWithPostProcessor.java new file mode 100644 index 000000000000..491a3096d1e2 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingpostprocessor/JmsClientWithPostProcessor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.jmssendingpostprocessor; + + +import jakarta.jms.ConnectionFactory; + +import org.springframework.jms.core.JmsClient; +import org.springframework.messaging.Message; +import org.springframework.messaging.core.MessagePostProcessor; +import org.springframework.messaging.support.MessageBuilder; + +public class JmsClientWithPostProcessor { + + private final JmsClient jmsClient; + + public JmsClientWithPostProcessor(ConnectionFactory connectionFactory) { + this.jmsClient = JmsClient.builder(connectionFactory) + .messagePostProcessor(new TenantIdMessageInterceptor("42")) + .build(); + } + + public void sendWithPostProcessor() { + this.jmsClient.destination("myQueue") + .withTimeToLive(1000) + .send("myPayload"); + } + + static class TenantIdMessageInterceptor implements MessagePostProcessor { + + private final String tenantId; + + public TenantIdMessageInterceptor(String tenantId) { + this.tenantId = tenantId; + } + + @Override + public Message postProcessMessage(Message message) { + return MessageBuilder.fromMessage(message) + .setHeader("tenantId", this.tenantId) + .build(); + } + } +} 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 index cd10a82ac930..715f2dc9d20e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index bdc8c847e2e3..5a8c5ccbc41b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index d227fba9dbb3..7f139785d091 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 70f3a08667c2..38f07d7a6c0f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 4100da2d922f..381944b5ab9e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 98ce1302a694..8c897d859915 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 6412ecabf99a..f777e0f233eb 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index b1dab51d7925..56e3b4e31597 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 0d01074c74b6..b4a0a3c5a0c3 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 723340868d4e..19b329cb729a 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 27c53e0d6a53..fdcc22354241 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 6835d2f0afe0..a56646e0db91 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/ApplicationEventsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/ApplicationEventsConfiguration.java index bdcf1b152efc..4ef357439bd4 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/ApplicationEventsConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/ApplicationEventsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailNotificationListener.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailNotificationListener.java index e115bdddc4b8..780811ef8ba7 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailNotificationListener.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailNotificationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailReceivedEvent.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailReceivedEvent.java index 1e9e27b52cbb..100c79edc22e 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailReceivedEvent.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EmailReceivedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EventAsyncExecutionConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EventAsyncExecutionConfiguration.java index 4c07472bd5d0..ec9245cb9833 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EventAsyncExecutionConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/applicationevents/EventAsyncExecutionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..a16bad0698af 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-present the original author 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/observability/config/conventions/ExtendedServerRequestObservationConvention.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ExtendedServerRequestObservationConvention.java index 48049a2c7552..d07b117678dd 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ExtendedServerRequestObservationConvention.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ExtendedServerRequestObservationConvention.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ServerRequestObservationFilter.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ServerRequestObservationFilter.java index fabad6d79f90..f85d01cbf273 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ServerRequestObservationFilter.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ServerRequestObservationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/reactive/HttpHandlerConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/reactive/HttpHandlerConfiguration.java index dab8da25d5c0..4dbad8717c9f 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/reactive/HttpHandlerConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/reactive/HttpHandlerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/reactive/UserController.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/reactive/UserController.java index 67b035f7f3e6..164f907ee33a 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/reactive/UserController.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/reactive/UserController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/servlet/UserController.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/servlet/UserController.java index 033992720177..48b3232d6f9f 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/servlet/UserController.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/httpserver/servlet/UserController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/process/JmsConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/process/JmsConfiguration.java index d611d3ed8153..204b1bf1c93e 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/process/JmsConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/process/JmsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/publish/JmsTemplatePublish.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/publish/JmsTemplatePublish.java index 4cc828714f7c..10cbd70ff1d9 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/publish/JmsTemplatePublish.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/jms/publish/JmsTemplatePublish.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/tasksscheduled/ObservationSchedulingConfigurer.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/tasksscheduled/ObservationSchedulingConfigurer.java index 931f362890dc..b2f8be4b6b12 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/observability/tasksscheduled/ObservationSchedulingConfigurer.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/tasksscheduled/ObservationSchedulingConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.java b/framework-docs/src/main/java/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.java new file mode 100644 index 000000000000..6982f682d0c4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resthttpserviceclient.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::httpserviceclient[] + public interface RepositoryService { + + @GetExchange("/repos/search") + List searchRepository(Search search); + + } + // end::httpserviceclient[] + + 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 index d38b7b4b4c9d..f273ea491f42 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..cde46e85c559 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/schedulingtaskexecutorusage/LoggingTaskDecorator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index c3beb0be046c..8065ff716ec2 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,4 +38,13 @@ 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 index 62a156cff177..58db3e628433 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..457800a0b4a1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertions/HotelControllerTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..24f642637324 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertionsjson/FamilyControllerTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..791adbf7aedb --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterintegration/HotelControllerTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..cf68b67eb56b --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequests/HotelControllerTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +/** + * @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..1017177b4d45 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsasync/AsyncControllerTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..c5bb45f815b1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsmultipart/MultipartControllerTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..23f8a61a9c0d --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestspaths/HotelControllerTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..fad8b9d6cbce --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountController.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..42a915db8620 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerIntegrationTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..605a27f7c43b --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerStandaloneTests.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..6ccdf4efa18f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/ApplicationWebConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..1c3132fa3ac4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.AbstractJacksonHttpMessageConverter ; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.test.web.servlet.assertj.MockMvcTester; +import org.springframework.web.context.WebApplicationContext; + +@SuppressWarnings("removal") +// tag::snippet[] +@SpringJUnitWebConfig(ApplicationWebConfiguration.class) +class AccountControllerIntegrationTests { + + private final MockMvcTester mockMvc; + + AccountControllerIntegrationTests(@Autowired WebApplicationContext wac) { + this.mockMvc = MockMvcTester.from(wac).withHttpMessageConverters( + List.of(wac.getBean(AbstractJacksonHttpMessageConverter.class))); + } + + // ... + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/assertj/AssertJTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/assertj/AssertJTests.java new file mode 100644 index 000000000000..84551e342ccc --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/assertj/AssertJTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.assertj; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.client.ExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AssertJTests { + + RestTestClient client; + + @Test + void withSpec() { + // tag::withSpec[] + RestTestClient.ResponseSpec spec = client.get().uri("/persons").exchange(); + + RestTestClientResponse response = RestTestClientResponse.from(spec); + assertThat(response).hasStatusOk(); + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); + // end::withSpec[] + } + + @Test + void withResult() { + // tag::withResult[] + ExchangeResult result = client.get().uri("/persons").exchange().returnResult(); + + RestTestClientResponse response = RestTestClientResponse.from(result); + assertThat(response).hasStatusOk(); + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); + // end::withResult[] + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/contextconfig/RestClientContextTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/contextconfig/RestClientContextTests.java new file mode 100644 index 000000000000..35a879546256 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/contextconfig/RestClientContextTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.contextconfig; + +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.context.WebApplicationContext; + + +@SpringJUnitConfig(WebConfig.class) // Specify the configuration to load +public class RestClientContextTests { + + RestTestClient client; + + @BeforeEach + void setUp(WebApplicationContext context) { // Inject the configuration + // Create the `RestTestClient` + client = RestTestClient.bindToApplicationContext(context).build(); + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/contextconfig/WebConfig.java b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/contextconfig/WebConfig.java new file mode 100644 index 000000000000..5d90979a7cd4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/contextconfig/WebConfig.java @@ -0,0 +1,20 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.contextconfig; + +public class WebConfig { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/json/JsonTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/json/JsonTests.java new file mode 100644 index 000000000000..7aa846153903 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/json/JsonTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.json; + +import org.junit.jupiter.api.Test; + +import org.springframework.test.web.servlet.client.RestTestClient; + +public class JsonTests { + + RestTestClient client; + + @Test + void jsonBody() { + // tag::jsonBody[] + client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody() + .json("{\"name\":\"Jane\"}"); + // end::jsonBody[] + } + + @Test + void jsonPath() { + // tag::jsonPath[] + client.get().uri("/persons") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$[0].name").isEqualTo("Jane") + .jsonPath("$[1].name").isEqualTo("Jason"); + // end::jsonPath[] + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/multipart/MultipartTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/multipart/MultipartTests.java new file mode 100644 index 000000000000..9f58cd698d50 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/multipart/MultipartTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.multipart; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.http.converter.multipart.FilePart; +import org.springframework.http.converter.multipart.FormFieldPart; +import org.springframework.http.converter.multipart.Part; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MultipartTests { + + RestTestClient client; + + @Test + void multipart() { + // tag::multipart[] + client.get().uri("/upload") + .accept(MediaType.MULTIPART_FORM_DATA) + .exchange() + .expectStatus().isOk() + .expectBody(new ParameterizedTypeReference>() {}) + .value(result -> { + Part field = result.getFirst("fieldPart"); + assertThat(field).isInstanceOfSatisfying(FormFieldPart.class, + formField -> assertThat(formField.value()).isEqualTo("fieldValue")); + Part file = result.getFirst("filePart"); + assertThat(file).isInstanceOfSatisfying(FilePart.class, + filePart -> assertThat(filePart.filename()).isEqualTo("logo.png")); + }); + // end::multipart[] + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/nocontent/NoContentTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/nocontent/NoContentTests.java new file mode 100644 index 000000000000..b48ad16d570c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/nocontent/NoContentTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.nocontent; + +import org.junit.jupiter.api.Test; + +import org.springframework.test.web.servlet.client.RestTestClient; + +public class NoContentTests { + + + RestTestClient client; + + @Test + void emptyBody() { + Person person = new Person("Jane"); + // tag::emptyBody[] + client.post().uri("/persons") + .body(person) + .exchange() + .expectStatus().isCreated() + .expectBody().isEmpty(); + // end::emptyBody[] + } + + @Test + void ignoreBody() { + Person person = new Person("Jane"); + // tag::ignoreBody[] + client.post().uri("/persons") + .body(person) + .exchange() + .expectStatus().isCreated() + .expectBody(Void.class); + // end::ignoreBody[] + } + + record Person(String name) { + + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/workflow/RestClientWorkflowTests.java b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/workflow/RestClientWorkflowTests.java new file mode 100644 index 000000000000..2f67b39156f2 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/testing/resttestclient/workflow/RestClientWorkflowTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.workflow; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.client.EntityExchangeResult; +import org.springframework.test.web.servlet.client.RestTestClient; + +public class RestClientWorkflowTests { + + RestTestClient client; + + @Test + void workflowTest() { + // tag::test[] + client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody(); + // end::test[] + } + + @Test + void softAssertions() { + // tag::soft-assertions[] + client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectAll( + spec -> spec.expectStatus().isOk(), + spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) + ); + // end::soft-assertions[] + } + + @Test + void consumeWith() { + // tag::consume[] + client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody(Person.class) + .consumeWith(result -> { + // custom assertions (for example, AssertJ)... + }); + // end::consume[] + } + + @Test + void returnResult() { + // tag::result[] + EntityExchangeResult result = client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody(Person.class) + .returnResult(); + + Person person = result.getResponseBody(); + HttpHeaders requestHeaders = result.getRequestHeaders(); + // end::result[] + } + + record Person(String name) { + + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/mvccorsglobal/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/mvccorsglobal/WebConfiguration.java new file mode 100644 index 000000000000..8d790e4bc3b8 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/mvccorsglobal/WebConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvccorsglobal; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/**") + .allowedOrigins("https://domain1.com", "https://domain2.com") + .allowedMethods("GET", "PUT") + .allowedHeaders("header1", "header2", "header3") + .exposedHeaders("header1", "header2") + .allowCredentials(true) + .maxAge(3600); + + // Add more mappings... + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java new file mode 100644 index 000000000000..714e0f71970a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java @@ -0,0 +1,69 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.annmethods.partevent; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.codec.multipart.FilePartEvent; +import org.springframework.http.codec.multipart.FormPartEvent; +import org.springframework.http.codec.multipart.PartEvent; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RestController +public class PartEventController { + + // tag::snippet[] + @PostMapping("/") + public void handle(@RequestBody Flux allPartEvents) { + + // The final PartEvent for a particular part will have isLast() set to true, and can be + // followed by additional events belonging to subsequent parts. + // This makes the isLast property suitable as a predicate for the Flux::windowUntil operator, to + // split events from all parts into windows that each belong to a single part. + allPartEvents.windowUntil(PartEvent::isLast) + // The Flux::switchOnFirst operator allows you to see whether you are handling + // a form field or file upload + .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { + if (signal.hasValue()) { + PartEvent event = signal.get(); + if (event instanceof FormPartEvent formEvent) { + String value = formEvent.value(); + // Handling of the form field + } + else if (event instanceof FilePartEvent fileEvent) { + String filename = fileEvent.filename(); + + // The body contents must be completely consumed, relayed, or released to avoid memory leaks + Flux contents = partEvents.map(PartEvent::content); + // Handling of the file upload + } + else { + return Mono.error(new RuntimeException("Unexpected event: " + event)); + } + } + else { + return partEvents; // either complete or error signal + } + return Mono.empty(); + })); + } + // 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 index 423f47a567a6..e03059e6f01f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 08e700360441..314ac66a02e4 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..72de9d0982c1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/filters/urlhandler/UrlHandlerFilterConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..fc14dce44367 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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/webfluxfnhandlerclasses/Person.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/Person.java new file mode 100644 index 000000000000..9a5e7fea9b8b --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/Person.java @@ -0,0 +1,19 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerclasses; + +public record Person(String name) { } diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.java new file mode 100644 index 000000000000..e8b00303b335 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerclasses; + +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +// tag::snippet[] +public class PersonHandler { + + private final PersonRepository repository; + + public PersonHandler(PersonRepository repository) { + this.repository = repository; + } + + // listPeople is a handler function that returns all Person objects found + // in the repository as JSON + public Mono listPeople(ServerRequest request) { + Flux people = repository.allPeople(); + return ok().contentType(APPLICATION_JSON).body(people, Person.class); + } + + // createPerson is a handler function that stores a new Person contained + // in the request body. + // Note that PersonRepository.savePerson(Person) returns Mono: an empty + // Mono that emits a completion signal when the person has been read from the + // request and stored. So we use the build(Publisher) method to send a + // response when that completion signal is received (that is, when the Person + // has been saved) + public Mono createPerson(ServerRequest request) { + Mono person = request.bodyToMono(Person.class); + return ok().build(repository.savePerson(person)); + } + + // getPerson is a handler function that returns a single person, identified by + // the id path variable. We retrieve that Person from the repository and create + // a JSON response, if it is found. If it is not found, we use switchIfEmpty(Mono) + // to return a 404 Not Found response. + public Mono getPerson(ServerRequest request) { + int personId = Integer.valueOf(request.pathVariable("id")); + return repository.getPerson(personId) + .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person)) + .switchIfEmpty(ServerResponse.notFound().build()); + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.java new file mode 100644 index 000000000000..34372f19847f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerclasses; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface PersonRepository { + + Flux allPeople(); + + Mono savePerson(Mono person); + + Mono getPerson(int id); +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.java new file mode 100644 index 000000000000..4ff62dbcd418 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerfilterfunction; + +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; + +public class RouterConfiguration { + + public RouterFunction route(PersonHandler handler) { + // tag::snippet[] + SecurityManager securityManager = getSecurityManager(); + + RouterFunction route = RouterFunctions.route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET(handler::listPeople)) + .POST(handler::createPerson)) + .filter((request, next) -> { + if (securityManager.allowAccessTo(request.path())) { + return next.handle(request); + } + else { + return ServerResponse.status(UNAUTHORIZED).build(); + } + }).build(); + // end::snippet[] + return route; + } + + SecurityManager getSecurityManager() { + return path -> false; + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.java new file mode 100644 index 000000000000..b2b24b435ef1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerfilterfunction; + +public interface SecurityManager { + + boolean allowAccessTo(String path); +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.java new file mode 100644 index 000000000000..849c7b8e06e4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlervalidation; + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person; +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebInputException; +import reactor.core.publisher.Mono; + +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +// tag::snippet[] +public class PersonHandler { + + // Create Validator instance + private final Validator validator = new PersonValidator(); + + private final PersonRepository repository; + + public PersonHandler(PersonRepository repository) { + this.repository = repository; + } + + public Mono createPerson(ServerRequest request) { + // Apply validation + Mono person = request.bodyToMono(Person.class).doOnNext(this::validate); + return ok().build(repository.savePerson(person)); + } + + private void validate(Person person) { + Errors errors = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, errors); + if (errors.hasErrors()) { + // Raise exception for a 400 response + throw new ServerWebInputException(errors.toString()); + } + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.java new file mode 100644 index 000000000000..0ea8f8494649 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlervalidation; + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class PersonValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return Person.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + // Validation logic + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.java new file mode 100644 index 000000000000..42c8aac28ec2 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnpredicates; + +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; + +public class RouterConfiguration { + + public RouterFunction route() { + // tag::snippet[] + RouterFunction route = RouterFunctions.route() + .GET("/hello-world", accept(MediaType.TEXT_PLAIN), + request -> ServerResponse.ok().bodyValue("Hello World")).build(); + // end::snippet[] + return route; + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.java new file mode 100644 index 000000000000..23b7f23d323c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnrequest; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.codec.multipart.FilePartEvent; +import org.springframework.http.codec.multipart.FormPartEvent; +import org.springframework.http.codec.multipart.PartEvent; +import org.springframework.web.reactive.function.server.ServerRequest; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class PartEventHandler { + + public void handle(ServerRequest request) { + // tag::snippet[] + request.bodyToFlux(PartEvent.class).windowUntil(PartEvent::isLast) + .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { + if (signal.hasValue()) { + PartEvent event = signal.get(); + if (event instanceof FormPartEvent formEvent) { + String value = formEvent.value(); + // handle form field + } + else if (event instanceof FilePartEvent fileEvent) { + String filename = fileEvent.filename(); + Flux contents = partEvents.map(PartEvent::content); + // handle file upload + } + else { + return Mono.error(new RuntimeException("Unexpected event: " + event)); + } + } + else { + return partEvents; // either complete or error signal + } + return Mono.empty(); + })); + // end::snippet[] + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/RequestHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/RequestHandler.java new file mode 100644 index 000000000000..b23e6590bbac --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/RequestHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnrequest; + +import reactor.core.publisher.Mono; + +import org.springframework.web.reactive.function.server.ServerRequest; + +public class RequestHandler { + + public void bind(ServerRequest request) { + // tag::snippet[] + Mono pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name")); + // end::snippet[] + } + + record Pet(String name) { } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.java new file mode 100644 index 000000000000..37914babb122 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnresponse; + +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +public class ResponseHandler { + + public Mono createResponse() { + // tag::snippet[] + Mono person = getPerson(); + return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class); + // end::snippet[] + } + + private Mono getPerson() { + return Mono.just(new Person("foo")); + } + + record Person(String name) { } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.java new file mode 100644 index 000000000000..9e88ea2766c8 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnroutes; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person; +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler; +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +public class RouterConfiguration { + + public RouterFunction routes() { + // tag::snippet[] + PersonRepository repository = getPersonRepository(); + PersonHandler handler = new PersonHandler(repository); + + RouterFunction otherRoute = getOtherRoute(); + + RouterFunction route = route() + // GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson + .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) + // GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople + .GET("/person", accept(APPLICATION_JSON), handler::listPeople) + // POST /person with no additional predicates is mapped to PersonHandler.createPerson + .POST("/person", handler::createPerson) + // otherRoute is a router function that is created elsewhere and added to the route built + .add(otherRoute) + .build(); + // end::snippet[] + return route; + } + + PersonRepository getPersonRepository() { + return new PersonRepository() { + @Override + public Flux allPeople() { + return null; + } + + @Override + public Mono savePerson(Mono person) { + return null; + } + + @Override + public Mono getPerson(int id) { + return null; + } + }; + } + + RouterFunction getOtherRoute() { + return RouterFunctions.route().build(); + } +} 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..4297ad5fc6d6 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/filters/urlhandler/UrlHandlerFilterConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index f6f386efc17b..4e719501ad07 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index f3649e9accdb..7868a0e18542 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java new file mode 100644 index 000000000000..d1baeae21328 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigapiversion; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer.useRequestHeader("API-Version"); + } +} +// 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 index 9697847decbc..1d836dd239ec 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index efa8b5090ad9..d7d5d6901672 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ec7166f54f60..bab830d7b65a 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index f6aee7304608..ba05949480ee 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 8616dd994fbb..076a76a8d063 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a087cca04faf..51933d7ed648 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; -import org.springframework.web.servlet.theme.ThemeChangeInterceptor; // tag::snippet[] @Configuration @@ -29,7 +29,7 @@ public class WebConfiguration implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LocaleChangeInterceptor()); - registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**"); + registry.addInterceptor(new UserRoleAuthorizationInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**"); } } // end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java index 019a99a270cb..f5290333bd3a 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,29 +17,35 @@ package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigmessageconverters; import java.text.SimpleDateFormat; -import java.util.List; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import tools.jackson.dataformat.xml.XmlMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; 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.http.converter.HttpMessageConverters; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; +import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +@SuppressWarnings("removal") // 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())); + public void configureMessageConverters(HttpMessageConverters.ServerBuilder builder) { + JsonMapper jsonMapper = JsonMapper.builder() + .findAndAddModules() + .enable(SerializationFeature.INDENT_OUTPUT) + .defaultDateFormat(new SimpleDateFormat("yyyy-MM-dd")) + .build(); + XmlMapper xmlMapper = XmlMapper.builder() + .findAndAddModules() + .defaultUseWrapper(false) + .build(); + builder.withJsonConverter(new JacksonJsonHttpMessageConverter(jsonMapper)) + .withXmlConverter(new JacksonXmlHttpMessageConverter(xmlMapper)); } } // 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 index 10c07a4eab7c..607dbd3c36f1 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 46df090b4d13..81e6b69e5f19 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 5fc1c723632f..460e0ee005d0 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 0214a63ffcec..1b42c3c2eccb 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 5e8f46abc61e..0979c2cb93bd 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index f623fda64a6e..922e64c49639 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java index 0d949748a323..d28256ef38d5 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,15 +21,16 @@ 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; +import org.springframework.web.servlet.view.json.JacksonJsonView; +@SuppressWarnings("removal") // tag::snippet[] @Configuration public class FreeMarkerConfiguration implements WebMvcConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { - registry.enableContentNegotiation(new MappingJackson2JsonView()); + registry.enableContentNegotiation(new JacksonJsonView()); registry.freeMarker().cache(false); } diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java index c4d27f555ebd..4c99f525e8e8 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,16 @@ 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; +import org.springframework.web.servlet.view.json.JacksonJsonView; +@SuppressWarnings("removal") // tag::snippet[] @Configuration public class WebConfiguration implements WebMvcConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { - registry.enableContentNegotiation(new MappingJackson2JsonView()); + registry.enableContentNegotiation(new JacksonJsonView()); registry.jsp(); } } 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 index 055263cac89c..ec6107bd07dd 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 49a110e68096..5e0c5a14d2ae 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 84aab1258cf4..bc03da47fe74 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 8da9c174e126..6216727cf34e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index c11617638db1..68135ff334f3 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 ExceptionController { // tag::narrow[] @ExceptionHandler({FileSystemException.class, RemoteException.class}) - public ResponseEntity handleIoException(IOException ex) { + public ResponseEntity handleIOException(IOException ex) { return ResponseEntity.internalServerError().body(ex.getMessage()); } // end::narrow[] 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 index feecf5f67735..574cd4b0ac3e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/MyConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/MyConfiguration.java new file mode 100644 index 000000000000..8c7373a64b42 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/MyConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcannrequestmappingregistration; + +import java.lang.reflect.Method; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +// tag::snippet[] +@Configuration +public class MyConfiguration { + + // Inject the target handler and the handler mapping for controllers + @Autowired + public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) + throws NoSuchMethodException { + + // Prepare the request mapping meta data + RequestMappingInfo info = RequestMappingInfo + .paths("/user/{id}").methods(RequestMethod.GET).build(); + + // Get the handler method + Method method = UserHandler.class.getMethod("getUser", Long.class); + + // Add the registration + mapping.registerMapping(info, handler, method); + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/UserHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/UserHandler.java new file mode 100644 index 000000000000..51d6c7e242ea --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/UserHandler.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcannrequestmappingregistration; + +public class UserHandler { + + public void getUser(Long id) { + // ... + } +} + diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/AppConfig.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/AppConfig.java new file mode 100644 index 000000000000..a289fde35dd6 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/AppConfig.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/MyWebApplicationInitializer.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/MyWebApplicationInitializer.java new file mode 100644 index 000000000000..967c43567a88 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/MyWebApplicationInitializer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; + +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +// tag::snippet[] +public class MyWebApplicationInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext servletContext) { + + // Load Spring web application configuration + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(AppConfig.class); + + // Create and register the DispatcherServlet + DispatcherServlet servlet = new DispatcherServlet(context); + ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet); + registration.setLoadOnStartup(1); + registration.addMapping("/app/*"); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcanncustomerservletcontainererrorpage/ErrorController.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcanncustomerservletcontainererrorpage/ErrorController.java new file mode 100644 index 000000000000..50ab373a8d38 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcanncustomerservletcontainererrorpage/ErrorController.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvcanncustomerservletcontainererrorpage; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +// tag::snippet[] +@RestController +public class ErrorController { + + @RequestMapping(path = "/error") + public Map handle(HttpServletRequest request) { + Map map = new HashMap<>(); + map.put("status", request.getAttribute("jakarta.servlet.error.status_code")); + map.put("reason", request.getAttribute("jakarta.servlet.error.message")); + return map; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyFilterDispatcherServletInitializer.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyFilterDispatcherServletInitializer.java new file mode 100644 index 000000000000..6e0a1263e6c7 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyFilterDispatcherServletInitializer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvccontainerconfig; + +import jakarta.servlet.Filter; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import org.springframework.web.filter.HiddenHttpMethodFilter; +import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer; + +// tag::snippet[] +public class MyFilterDispatcherServletInitializer extends AbstractDispatcherServletInitializer { + + @Override + protected Filter[] getServletFilters() { + return new Filter[] { + new HiddenHttpMethodFilter(), new CharacterEncodingFilter() }; + } + + // @fold:on + @Override + protected WebApplicationContext createServletApplicationContext() { + /**/return null; + } + + @Override + protected String[] getServletMappings() { + /**/return new String[] { "/" }; + } + + @Override + protected WebApplicationContext createRootApplicationContext() { + /**/return null; + } + // @fold:off +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebAppInitializer.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebAppInitializer.java new file mode 100644 index 000000000000..aa47dcba5a20 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebAppInitializer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvccontainerconfig; + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +// tag::snippet[] +public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class[] getRootConfigClasses() { + return null; + } + + @Override + protected Class[] getServletConfigClasses() { + return new Class[] { MyWebConfig.class }; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } +} +// end::snippet[] + +class MyWebConfig {} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebApplicationInitializer.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebApplicationInitializer.java new file mode 100644 index 000000000000..dd000a7b0a5f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebApplicationInitializer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvccontainerconfig; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.support.XmlWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +// tag::snippet[] +public class MyWebApplicationInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext container) { + XmlWebApplicationContext appContext = new XmlWebApplicationContext(); + appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml"); + + ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext)); + registration.setLoadOnStartup(1); + registration.addMapping("/"); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyXmlDispatcherServletInitializer.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyXmlDispatcherServletInitializer.java new file mode 100644 index 000000000000..fa16e9e6eb33 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyXmlDispatcherServletInitializer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvccontainerconfig; + +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.XmlWebApplicationContext; +import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer; + +// tag::snippet[] +public class MyXmlDispatcherServletInitializer extends AbstractDispatcherServletInitializer { + + @Override + protected WebApplicationContext createRootApplicationContext() { + return null; + } + + @Override + protected WebApplicationContext createServletApplicationContext() { + XmlWebApplicationContext cxt = new XmlWebApplicationContext(); + cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml"); + return cxt; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolvercookie/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolvercookie/WebConfiguration.java new file mode 100644 index 000000000000..7dd176297016 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolvercookie/WebConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvclocaleresolvercookie; + +import java.time.Duration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.CookieLocaleResolver; + +// tag::snippet[] +@Configuration +public class WebConfiguration { + + @Bean + public LocaleResolver localeResolver() { + CookieLocaleResolver localeResolver = new CookieLocaleResolver("clientlanguage"); + localeResolver.setCookieMaxAge(Duration.ofSeconds(100000)); + return localeResolver; + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolverinterceptor/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolverinterceptor/WebConfiguration.java new file mode 100644 index 000000000000..d63f1c7a9878 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolverinterceptor/WebConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvclocaleresolverinterceptor; + +import java.util.Map; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.i18n.CookieLocaleResolver; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; + +// tag::snippet[] +@Configuration +public class WebConfiguration { + + @Bean + public LocaleResolver localeResolver() { + return new CookieLocaleResolver(); + } + + @Bean + public SimpleUrlHandlerMapping urlMapping() { + SimpleUrlHandlerMapping urlHandlerMapping = new SimpleUrlHandlerMapping(); + LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor(); + interceptor.setParamName("siteLanguage"); + urlHandlerMapping.setInterceptors(interceptor); + urlHandlerMapping.setUrlMap(Map.of("/**/*.view", "someController")); + return urlHandlerMapping; + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcloggingsensitivedata/MyInitializer.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcloggingsensitivedata/MyInitializer.java new file mode 100644 index 000000000000..45063827faa7 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcloggingsensitivedata/MyInitializer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvcloggingsensitivedata; + +import jakarta.servlet.ServletRegistration; + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +// tag::snippet[] +public class MyInitializer + extends AbstractAnnotationConfigDispatcherServletInitializer { + + // @fold:on + @Override + protected Class[] getRootConfigClasses() { + /**/throw new UnsupportedOperationException(); + } + + @Override + protected Class[] getServletConfigClasses() { + /**/throw new UnsupportedOperationException(); + } + + @Override + protected String[] getServletMappings() { + /**/throw new UnsupportedOperationException(); + } + + // @fold:off + @Override + protected void customizeRegistration(ServletRegistration.Dynamic registration) { + registration.setInitParameter("enableLoggingRequestDetails", "true"); + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcmultipartresolverstandard/AppInitializer.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcmultipartresolverstandard/AppInitializer.java new file mode 100644 index 000000000000..9f352e62d377 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcmultipartresolverstandard/AppInitializer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvcmultipartresolverstandard; + +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.ServletRegistration; +import org.jspecify.annotations.Nullable; + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +// tag::snippet[] +public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + // @fold:on + @Override + protected String[] getServletMappings() { + /**/throw new UnsupportedOperationException(); + } + + @Override + protected Class @Nullable [] getRootConfigClasses() { + /**/throw new UnsupportedOperationException(); + } + + @Override + protected Class @Nullable [] getServletConfigClasses() { + /**/throw new UnsupportedOperationException(); + } + + // @fold:off + @Override + protected void customizeRegistration(ServletRegistration.Dynamic registration) { + + // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold + registration.setMultipartConfig(new MultipartConfigElement("/tmp")); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcservletcontexthierarchy/MyWebAppInitializer.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcservletcontexthierarchy/MyWebAppInitializer.java new file mode 100644 index 000000000000..f8b28b86943d --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcservlet/mvcservletcontexthierarchy/MyWebAppInitializer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvcservletcontexthierarchy; + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +// tag::snippet[] +public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class[] getRootConfigClasses() { + return new Class[] { RootConfig.class }; + } + + @Override + protected Class[] getServletConfigClasses() { + return new Class[] { App1Config.class }; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/app1/*" }; + } +} +// end::snippet[] + +class RootConfig {} +class App1Config {} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvcfnrunning/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvcfnrunning/WebConfiguration.java new file mode 100644 index 000000000000..b21cd3773813 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvcfnrunning/WebConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcfnrunning; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverters; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.function.RouterFunction; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Bean + public RouterFunction routerFunctionA() { + // ... + return null; + } + + @Bean + public RouterFunction routerFunctionB() { + // ... + return null; + } + + @Override + public void configureMessageConverters(HttpMessageConverters.ServerBuilder builder) { + // configure message conversion... + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + // configure CORS... + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + // configure view resolution for HTML rendering... + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewfreemarkercontextconfig/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewfreemarkercontextconfig/WebConfiguration.java new file mode 100644 index 000000000000..8ab63786a14e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewfreemarkercontextconfig/WebConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewfreemarkercontextconfig; + +import java.nio.charset.StandardCharsets; + +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; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + // Configure FreeMarker... + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/WEB-INF/freemarker"); + configurer.setDefaultCharset(StandardCharsets.UTF_8); + return configurer; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewgroovymarkupconfiguration/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewgroovymarkupconfiguration/WebConfiguration.java new file mode 100644 index 000000000000..621328481778 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewgroovymarkupconfiguration/WebConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewgroovymarkupconfiguration; + +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.groovy.GroovyMarkupConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.groovy(); + } + + // Configure the Groovy Markup Template Engine... + + @Bean + public GroovyMarkupConfigurer groovyMarkupConfigurer() { + GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer(); + configurer.setResourceLoaderPath("/WEB-INF/"); + return configurer; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewjspresolver/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewjspresolver/WebConfiguration.java new file mode 100644 index 000000000000..fbc7d1286414 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewjspresolver/WebConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewjspresolver; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.jsp(); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewscriptintegrate/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewscriptintegrate/WebConfiguration.java new file mode 100644 index 000000000000..1c3146fc0982 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewscriptintegrate/WebConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewscriptintegrate; + +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.script.ScriptTemplateConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.scriptTemplate(); + } + + @Bean + public ScriptTemplateConfigurer configurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("jython"); + configurer.setScripts("render.py"); + configurer.setRenderFunction("render"); + return configurer; + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewsfreemarker/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewsfreemarker/WebConfiguration.java new file mode 100644 index 000000000000..38fdf3afd6a1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewsfreemarker/WebConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewsfreemarker; + +import java.util.Map; + +import freemarker.template.utility.XmlEscape; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; + +@Configuration +public class WebConfiguration { + + // tag::snippet[] + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/WEB-INF/freemarker"); + configurer.setFreemarkerVariables(Map.of("xml_escape", new XmlEscape())); + return configurer; + } + // end::snippet[] +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewxsltbeandefs/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewxsltbeandefs/WebConfiguration.java new file mode 100644 index 000000000000..9e2a47b263d0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvcview/mvcviewxsltbeandefs/WebConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewxsltbeandefs; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.view.xslt.XsltViewResolver; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Bean + public XsltViewResolver xsltViewResolver() { + XsltViewResolver viewResolver = new XsltViewResolver(); + viewResolver.setPrefix("/WEB-INF/xsl/"); + viewResolver.setSuffix(".xslt"); + return viewResolver; + } +} +// end::snippet[] 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 index 2fb295375b2e..3e54feccff3a 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 303e245f24f8..82b4a4fe39bb 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,7 @@ 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; 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 index 82d556dd59de..be13be36c8ba 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,7 @@ 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; 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 index 70f6ce808900..765b2471bd2c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 269f61bb800c..d0537c3e9c08 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 0b282736aaaa..a561f4541e02 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 20ddf32694f9..a66e9862d9d9 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 926da7d78032..f05b97308176 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 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.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; // tag::snippet[] @@ -41,7 +40,7 @@ public void configureMessageBroker(MessageBrokerRegistry registry) { private ReactorNettyTcpClient createTcpClient() { return new ReactorNettyTcpClient<>( - client -> client.addressSupplier(() -> new InetSocketAddress(0)), + client -> client.remoteAddress(() -> new InetSocketAddress(0)), new StompReactorNettyCodec()); } } 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 index b6518257ce66..5dca85de2095 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import 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[] 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 index 721d631301ab..106a64bfe8f0 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 31082f7991d2..c5f3f060ccef 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 9f8302ac11f2..d5c5228fff54 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,8 @@ 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 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 index 4198cc93e94a..c6b751f6c8e4 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 527d2bb7bab0..f77e581a24f6 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 9f787007bf64..b5e58f79dc8c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.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; 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 index 78e4c99773e9..2f678e8a06ce 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index b43079e82b04..bafe4629ba44 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketfallbacksockjsclient/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketfallbacksockjsclient/WebSocketConfiguration.java new file mode 100644 index 000000000000..9ba6f8bfc182 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketfallbacksockjsclient/WebSocketConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.websocketfallbacksockjsclient; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurationSupport; + +// tag::snippet[] +@Configuration +public class WebSocketConfiguration extends WebSocketMessageBrokerConfigurationSupport { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/sockjs").withSockJS() + // Set the streamBytesLimit property to 512KB (the default is 128KB -- 128 * 1024) + .setStreamBytesLimit(512 * 1024) + // Set the httpMessageCacheSize property to 1,000 (the default is 100) + .setHttpMessageCacheSize(1000) + // Set the disconnectDelay property to 30 property seconds (the default is five seconds -- 5 * 1000) + .setDisconnectDelay(30 * 1000); + } + + // ... +} +// 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 index dde6939608b6..58c6f67aa957 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketfallbackxhrvsiframe/WebSocketConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketfallbackxhrvsiframe/WebSocketConfiguration.java new file mode 100644 index 000000000000..85df5e29781a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/websocket/websocketfallbackxhrvsiframe/WebSocketConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.websocketfallbackxhrvsiframe; + +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() + .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js"); + } + + // ... + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // Configure message broker... + } +} +// 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 index 632136a0dacf..35569a72193b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ed4be8e7d7f1..c8d7ae41c9b0 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 753a598eedaf..2bdee9217b3a 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 87b5d64325f5..b2c65b545fda 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 07897a2a2d31..c984f75b55d5 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 76f78d5e9785..c1de2ea82157 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 95c74290c5c0..92d245c789c8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 8c5635a64111..d9f381bfe360 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 587efae332a1..50e99588400b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ff92f4e372ef..d4e98924915c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ca3544a39920..85f41c169e63 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ebeb56710f92..155e2aa30147 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 53942988ecbd..df776368bfef 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 3e961535846c..a7befa7060d5 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index c4f918f51fec..c8c81d8c4e90 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,6 +54,6 @@ class ConcurrentOperationExecutor : Ordered { lockFailureException = ex } } while (numAttempts <= this.maxRetries) - throw lockFailureException!! + 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 index 89224a8d4884..5a18b86325a8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index da5131d74869..bb9bff91bc94 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 16a6cba610bb..ad7b242a7638 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 2ae08344f64f..1dc966899f56 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..5aa1dfb11e9e --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index cf19c8859a7e..3ea0d71bcb16 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..a109bbb7be97 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 0cd083e0fdb1..80e5a2255767 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Bar.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Bar.kt new file mode 100644 index 000000000000..eeeb6a546288 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Bar.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.java.beansjavaprogrammaticregistration + +data class Bar(val foo: Foo) \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Baz.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Baz.kt new file mode 100644 index 000000000000..0dab54a5c545 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Baz.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.java.beansjavaprogrammaticregistration + +data class Baz(val value: String) \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Foo.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Foo.kt new file mode 100644 index 000000000000..0942b2193a5b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Foo.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.java.beansjavaprogrammaticregistration + +class Foo \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.kt new file mode 100644 index 000000000000..51a898fb7f46 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.java.beansjavaprogrammaticregistration + +import org.springframework.beans.factory.BeanRegistrarDsl +import org.springframework.web.servlet.function.router + +// tag::snippet[] +class MyBeanRegistrar : BeanRegistrarDsl({ + registerBean() + registerBean( + name = "bar", + prototype = true, + lazyInit = true, + description = "Custom description") { + Bar(bean()) // Also possible with Bar(bean()) + } + profile("baz") { + registerBean { Baz("Hello World!") } + } + registerBean() + registerBean { + myRouter(bean()) // Also possible with myRouter(bean()) + } +}) + +fun myRouter(myRepository: MyRepository) = router { + // ... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.kt new file mode 100644 index 000000000000..81f7c29fc84e --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.java.beansjavaprogrammaticregistration + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import + +// tag::snippet[] +@Configuration +@Import(MyBeanRegistrar::class) +class MyConfiguration { +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyRepository.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyRepository.kt new file mode 100644 index 000000000000..e40ad268baf9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyRepository.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.java.beansjavaprogrammaticregistration + +interface MyRepository { +} \ 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..0dacb58f5fb5 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/CustomerPreferenceDao.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 4130074c621f..750cadc44cae 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..395f825e8359 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/MovieFinder.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 6dd298a7bc7a..0325b7b87e65 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index e6dc38a729aa..517475030c1d 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ee79955657da..523bb0feb9f4 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ee713a26d5af..0c27c13d5594 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index f863b880c75f..21ee9f950b5e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/languageref/expressionsoperatorsoverloaded/ListConcatenation.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/languageref/expressionsoperatorsoverloaded/ListConcatenation.kt new file mode 100644 index 000000000000..fbe430ddd860 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/languageref/expressionsoperatorsoverloaded/ListConcatenation.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.languageref.expressionsoperatorsoverloaded + +import org.springframework.expression.Operation +import org.springframework.expression.OperatorOverloader + +class ListConcatenation: OperatorOverloader { + + override fun overridesOperation(operation: Operation, left: Any?, right: Any?): Boolean { + return operation == Operation.ADD && left is List<*> && right is List<*> + } + + override fun operate(operation: Operation, left: Any?, right: Any?): Any { + if (operation == Operation.ADD && left is List<*> && right is List<*>) { + return left + right + } + + throw UnsupportedOperationException( + "No overload for operation $operation and operands [$left] and [$right]") + } + +} 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 index c4688db94871..5fa422dc5c66 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..bc9467c15399 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/validationbeanvalidationspringmethod/ApplicationConfiguration.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..ca83562e49ae --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/validation/validationbeanvalidationspringmethodexceptions/ApplicationConfiguration.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..29d4f775a2df --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/SqlTypeValueFactory.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..f0acfdd1034b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..fa40ebbdf739 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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/DriverManagerDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt index 87692df00b05..4ae60644ad1f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 7eab9f733af1..b0686a41b6f8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..04ce2e273fd6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventDao.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 0591b7c2765a..f14369d465d8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ceb82cc7e0c7..191c688ffdbb 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 0c3e8c11a079..8258ca8f4fd9 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/AppConfig.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/AppConfig.kt new file mode 100644 index 000000000000..cba3522fb4cd --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/AppConfig.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.transaction.declarative.transactiondeclarativeannotations + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.DataSourceTransactionManager +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.annotation.EnableTransactionManagement +import javax.sql.DataSource + +// tag::snippet[] +@Configuration +@EnableTransactionManagement +class AppConfig { + + @Bean + fun fooService(): FooService { + return DefaultFooService() + } + + @Bean + fun txManager(dataSource: DataSource): PlatformTransactionManager { + return DataSourceTransactionManager(dataSource) + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/DefaultFooService.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/DefaultFooService.kt new file mode 100644 index 000000000000..9fcccf51c486 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/DefaultFooService.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.transaction.declarative.transactiondeclarativeannotations + +class DefaultFooService : FooService { +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/FooService.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/FooService.kt new file mode 100644 index 000000000000..7ee5d6ca08da --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/FooService.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.transaction.declarative.transactiondeclarativeannotations + +interface FooService { +} \ No newline at end of file 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 index 674b8aad85c6..65503c57bfbd 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 8e85c14bbbe9..8d44dd1961ce 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.kt index f91e78a54875..a5184b689107 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,7 @@ class CustomCacheConfiguration { // tag::snippet[] @Bean fun cacheManager(): CacheManager { - return CaffeineCacheManager().apply { - cacheNames = listOf("default", "books") - } + return CaffeineCacheManager("default", "books") } // end::snippet[] } \ No newline at end of file diff --git a/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 index 779b1168eb95..2f96c3a69973 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 308f60cd7cfd..ad754157e2bb 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 5fc16ea91edc..5d69dfd5e0e2 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 2972d22898d6..7d80f6a5c0ee 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 9127de9c7a47..ce32ef1e1699 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index d7fecece981b..5a169001c49c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 002813ae7b4c..0f058b05fa46 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 2b0975e2778c..69f5d8b31ba2 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index c28c9a1ec6c0..8498e25715d3 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index fd4c58b6da22..acf34bd65206 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 3a4ab53c5e2c..0168a6ee0ddf 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 14ff4c8c515b..e4aa6dd784b9 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index fe55a8e740d0..c792cfc2e5ab 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index b77d0dc85612..9b635927a491 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 7f6ba5eacc3f..f49d4ec32d1c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index d90a04b9b3ef..a65ee5cc95e8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 1805e2c373b9..9455caf9f1ee 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ce0a5b085cbf..ca247ff727ca 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 98d9e5f04551..6f8385cdbc37 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 9f88e70ced10..a69a5778ac8b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..bd93c0a89a25 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 1f3210110e45..2312fe94fdce 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.kt index a4936ed4d947..fd61f5cf03fa 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,24 +19,8 @@ package org.springframework.docs.integration.jmx.jmxexporting // tag::snippet[] class JmxTestBean : IJmxTestBean { - private lateinit var name: String - private var age = 0 - - override fun getAge(): Int { - return age - } - - override fun setAge(age: Int) { - this.age = age - } - - override fun setName(name: String) { - this.name = name - } - - override fun getName(): String { - return name - } + override lateinit var name: String + override var age = 0 override fun add(x: Int, y: Int): Int { return x + y diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusage/OrderManager.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusage/OrderManager.kt index 08f7b80d12d4..4aff7d2c9f3a 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..f6f5f9e56b9a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/Customer.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index a4f0378da92c..fe2440e8627f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..86c5732f2952 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/Order.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 7be7aa437df3..73c2c6a9412d 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.kt new file mode 100644 index 000000000000..8f608e2d182c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resthttpserviceclient.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::httpserviceclient[] + interface RepositoryService { + + @GetExchange("/repos/search") + fun searchRepository(search: Search): List + + } + // end::httpserviceclient[] + + 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 index 4faed328fb37..7e36373a14ec 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..a1722e204e45 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/schedulingtaskexecutorusage/LoggingTaskDecorator.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 1a2dd1a8861d..c8276e7e30f2 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,4 +34,11 @@ class TaskExecutorConfiguration { @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 index 4ec8a261dd9e..d70e4d9581d0 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/languages/kotlin/coroutines/propagation/ContextPropagationSample.kt b/framework-docs/src/main/kotlin/org/springframework/docs/languages/kotlin/coroutines/propagation/ContextPropagationSample.kt new file mode 100644 index 000000000000..0de429ba57f6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/languages/kotlin/coroutines/propagation/ContextPropagationSample.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.languages.kotlin.coroutines.propagation + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.apache.commons.logging.Log +import org.apache.commons.logging.LogFactory +import org.springframework.core.PropagationContextElement + +class ContextPropagationSample { + + companion object { + private val logger: Log = LogFactory.getLog( + ContextPropagationSample::class.java + ) + } + + // tag::context[] + fun main() { + runBlocking(Dispatchers.IO + PropagationContextElement()) { + waitAndLog() + } + } + + suspend fun waitAndLog() { + delay(10) + logger.info("Suspending function with traceId") + } + // end::context[] +} \ 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..c35e19fd5eb9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertions/HotelControllerTests.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..e605605b9c20 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterassertionsjson/FamilyControllerTests.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..1c9b06c03911 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterintegration/HotelController.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.springframework.test.web.servlet.assertj.MockMvcTester +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +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..e40288047ce8 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequests/HotelControllerTests.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..19431e0e35d6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsasync/AsyncControllerTests.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..660a4eeb023e --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestsmultipart/MultipartControllerTests.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..40eb6d2510dd --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctesterrequestspaths/HotelControllerTests.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..7af942f11e42 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountController.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..fa7b60ab0292 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerIntegrationTests.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..49bba430e8f8 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountControllerStandaloneTests.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..027491b5ac89 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/ApplicationWebConfiguration.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..11f76bd86ec7 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/converter/AccountControllerIntegrationTests.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.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.AbstractJacksonHttpMessageConverter +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(AbstractJacksonHttpMessageConverter::class.java))) + + // ... + +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/assertj/AssertJTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/assertj/AssertJTests.kt new file mode 100644 index 000000000000..076c59f4e9fd --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/assertj/AssertJTests.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.assertj + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.client.RestTestClient +import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse + +class AssertJTests { + + + lateinit var client: RestTestClient + + @Test + fun withSpec() { + // tag::withSpec[] + val spec = client.get().uri("/persons").exchange() + + val response = RestTestClientResponse.from(spec) + Assertions.assertThat(response).hasStatusOk() + Assertions.assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) + // end::withSpec[] + } + + @Test + fun withResult() { + // tag::withResult[] + val result = client.get().uri("/persons").exchange().returnResult() + + val response = RestTestClientResponse.from(result) + Assertions.assertThat(response).hasStatusOk() + Assertions.assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) + // end::withResult[] + } + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/contextconfig/RestClientContextTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/contextconfig/RestClientContextTests.kt new file mode 100644 index 000000000000..956c34a17251 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/contextconfig/RestClientContextTests.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.contextconfig + +import org.junit.jupiter.api.BeforeEach +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig +import org.springframework.test.web.servlet.client.RestTestClient +import org.springframework.web.context.WebApplicationContext + +@SpringJUnitConfig(WebConfig::class) // Specify the configuration to load +class RestClientContextTests { + + lateinit var client: RestTestClient + + @BeforeEach + fun setUp(context: WebApplicationContext) { // Inject the configuration + // Create the `RestTestClient` + client = RestTestClient.bindToApplicationContext(context).build() + } +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/contextconfig/WebConfig.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/contextconfig/WebConfig.kt new file mode 100644 index 000000000000..5884e34c8da5 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/contextconfig/WebConfig.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.contextconfig + +class WebConfig { +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/json/JsonTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/json/JsonTests.kt new file mode 100644 index 000000000000..d7e935cb8c20 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/json/JsonTests.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.json + +import org.junit.jupiter.api.Test +import org.springframework.test.web.servlet.client.RestTestClient + +class JsonTests { + + lateinit var client: RestTestClient + + @Test + fun jsonBody() { + // tag::jsonBody[] + client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody() + .json("{\"name\":\"Jane\"}") + // end::jsonBody[] + } + + @Test + fun jsonPath() { + // tag::jsonPath[] + client.get().uri("/persons") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$[0].name").isEqualTo("Jane") + .jsonPath("$[1].name").isEqualTo("Jason") + // end::jsonPath[] + } + +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/nocontent/NoContentTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/nocontent/NoContentTests.kt new file mode 100644 index 000000000000..6fbbf22baa0e --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/nocontent/NoContentTests.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.nocontent + +import org.junit.jupiter.api.Test +import org.springframework.test.web.servlet.client.RestTestClient +import org.springframework.test.web.servlet.client.expectBody + +class NoContentTests { + + lateinit var client: RestTestClient + + @Test + fun emptyBody() { + val person = Person("Jane") + // tag::emptyBody[] + client.post().uri("/persons") + .body(person) + .exchange() + .expectStatus().isCreated() + .expectBody().isEmpty() + // end::emptyBody[] + } + + @Test + fun ignoreBody() { + val person = Person("Jane") + // tag::ignoreBody[] + client.get().uri("/persons/123") + .exchange() + .expectStatus().isNotFound + .expectBody() + // end::ignoreBody[] + } + + data class Person(val name: String) + +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/workflow/RestClientWorkflowTests.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/workflow/RestClientWorkflowTests.kt new file mode 100644 index 000000000000..32a8b13d6a00 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/resttestclient/workflow/RestClientWorkflowTests.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.resttestclient.workflow + +import org.junit.jupiter.api.Test +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.client.RestTestClient +import org.springframework.test.web.servlet.client.expectBody + +class RestClientWorkflowTests { + + lateinit var client: RestTestClient + + @Test + fun workflowTest() { + // tag::test[] + client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody() + // end::test[] + } + + @Test + fun softAssertions() { + // tag::soft-assertions[] + client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectAll( + { spec -> spec.expectStatus().isOk() }, + { spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) } + ) + // end::soft-assertions[] + } + + @Test + fun consumeWith() { + // tag::consume[] + client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody() + .consumeWith { + // custom assertions (for example, AssertJ)... + } + // end::consume[] + } + + @Test + fun returnResult() { + // tag::result[] + val result = client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk + .expectBody() + .returnResult() + + val person: Person? = result.responseBody + val requestHeaders = result.responseHeaders + // end::result[] + } + + data class Person(val name: String) + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/mvccorsglobal/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/mvccorsglobal/WebConfiguration.kt new file mode 100644 index 000000000000..cfa74d132543 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/mvccorsglobal/WebConfiguration.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvccorsglobal + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/api/**") + .allowedOrigins("https://domain1.com", "https://domain2.com") + .allowedMethods("GET", "PUT") + .allowedHeaders("header1", "header2", "header3") + .exposedHeaders("header1", "header2") + .allowCredentials(true) + .maxAge(3600) + + // Add more mappings... + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt new file mode 100644 index 000000000000..deed535d9777 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.annmethods.partevent + +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.http.codec.multipart.FilePartEvent +import org.springframework.http.codec.multipart.FormPartEvent +import org.springframework.http.codec.multipart.PartEvent +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +@RestController +class PartEventController { + + // tag::snippet[] + @PostMapping("/") + fun handle(@RequestBody allPartEvents: Flux) { + + // The final PartEvent for a particular part will have isLast() set to true, and can be + // followed by additional events belonging to subsequent parts. + // This makes the isLast property suitable as a predicate for the Flux::windowUntil operator, to + // split events from all parts into windows that each belong to a single part. + allPartEvents.windowUntil(PartEvent::isLast) + .concatMap { + + // The Flux::switchOnFirst operator allows you to see whether you are handling + // a form field or file upload + it.switchOnFirst { signal, partEvents -> + if (signal.hasValue()) { + val event = signal.get() + if (event is FormPartEvent) { + val value: String = event.value() + // Handling of the form field + } else if (event is FilePartEvent) { + val filename: String = event.filename() + + // The body contents must be completely consumed, relayed, or released to avoid memory leaks + val contents: Flux = partEvents.map(PartEvent::content) + // Handling of the file upload + } else { + return@switchOnFirst Mono.error(RuntimeException("Unexpected event: $event")) + } + } else { + return@switchOnFirst partEvents // either complete or error signal + } + Mono.empty() + } + } + } + // end::snippet[] + +} \ No newline at end of file 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 index 645aa1de0b4f..3b4bbf4f0059 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 3bddcc33f21a..d44901e4de3f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..f5ff60fe629b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/filters/urlhandler/UrlHandlerFilterConfiguration.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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..d5751b2383b6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/webfluxconfigpathmatching/WebConfig.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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/webfluxfnhandlerclasses/Person.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/Person.kt new file mode 100644 index 000000000000..c2336a54c6b8 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/Person.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerclasses + +data class Person(val name: String) diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.kt new file mode 100644 index 000000000000..7554aa5a47ff --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerclasses + +import kotlinx.coroutines.flow.Flow +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.awaitBody +import org.springframework.web.reactive.function.server.bodyAndAwait +import org.springframework.web.reactive.function.server.bodyValueAndAwait +import org.springframework.web.reactive.function.server.buildAndAwait + +// tag::snippet[] +class PersonHandler(private val repository: PersonRepository) { + + // listPeople is a handler function that returns all Person objects found + // in the repository as JSON + suspend fun listPeople(request: ServerRequest): ServerResponse { + val people: Flow = repository.allPeople() + return ServerResponse.ok().contentType(APPLICATION_JSON).bodyAndAwait(people) + } + + // createPerson is a handler function that stores a new Person contained + // in the request body. + // Note that PersonRepository.savePerson(Person) returns Mono: an empty + // Mono that emits a completion signal when the person has been read from the + // request and stored. So we use the build(Publisher) method to send a + // response when that completion signal is received (that is, when the Person + // has been saved) + suspend fun createPerson(request: ServerRequest): ServerResponse { + val person = request.awaitBody() + repository.savePerson(person) + return ServerResponse.ok().buildAndAwait() + } + + // getPerson is a handler function that returns a single person, identified by + // the id path variable. We retrieve that Person from the repository and create + // a JSON response, if it is found. If it is not found, we use switchIfEmpty(Mono) + // to return a 404 Not Found response. + suspend fun getPerson(request: ServerRequest): ServerResponse { + val personId = request.pathVariable("id").toInt() + return repository.getPerson(personId)?.let { ServerResponse.ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) } + ?: ServerResponse.notFound().buildAndAwait() + + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.kt new file mode 100644 index 000000000000..13963494c762 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerclasses + +import kotlinx.coroutines.flow.Flow + +interface PersonRepository { + + fun allPeople(): Flow + + suspend fun savePerson(person: Person) + + suspend fun getPerson(id: Int): Person? +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.kt new file mode 100644 index 000000000000..a9648256285a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerfilterfunction + +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler +import org.springframework.http.HttpStatus.UNAUTHORIZED +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.buildAndAwait +import org.springframework.web.reactive.function.server.coRouter + +class RouterConfiguration { + + fun route(handler: PersonHandler): RouterFunction { + // tag::snippet[] + val securityManager: SecurityManager = getSecurityManager() + + val route = coRouter { + ("/person" and accept(APPLICATION_JSON)).nest { + GET("/{id}", handler::getPerson) + GET("/", handler::listPeople) + POST("/", handler::createPerson) + filter { request, next -> + if (securityManager.allowAccessTo(request.path())) { + next(request) + } + else { + ServerResponse.status(UNAUTHORIZED).buildAndAwait() + } + } + } + } + // end::snippet[] + return route + } + +} + +fun getSecurityManager() = object : SecurityManager { + override fun allowAccessTo(path: String): Boolean { + TODO("Not yet implemented") + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.kt new file mode 100644 index 000000000000..2e2025a854e1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlerfilterfunction + +interface SecurityManager { + + fun allowAccessTo(path: String): Boolean +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.kt new file mode 100644 index 000000000000..206a6ad683b0 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlervalidation + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository +import org.springframework.validation.BeanPropertyBindingResult +import org.springframework.validation.Errors +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.awaitBody +import org.springframework.web.reactive.function.server.buildAndAwait +import org.springframework.web.server.ServerWebInputException + +// tag::snippet[] +class PersonHandler(private val repository: PersonRepository) { + + // Create Validator instance + private val validator = PersonValidator() + + suspend fun createPerson(request: ServerRequest): ServerResponse { + val person = request.awaitBody() + // Apply validation + validate(person) + repository.savePerson(person) + return ServerResponse.ok().buildAndAwait() + } + + private fun validate(person: Person) { + val errors: Errors = BeanPropertyBindingResult(person, "person") + validator.validate(person, errors) + if (errors.hasErrors()) { + // Raise exception for a 400 response + throw ServerWebInputException(errors.toString()) + } + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.kt new file mode 100644 index 000000000000..e5a7dd5112fd --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnhandlervalidation + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person +import org.springframework.validation.Errors +import org.springframework.validation.Validator + +class PersonValidator : Validator { + + override fun supports(clazz: Class<*>): Boolean { + return Person::class.java.isAssignableFrom(clazz) + } + + override fun validate(target: Any, errors: Errors) { + // Validation logic + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.kt new file mode 100644 index 000000000000..76d93b1444b9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnpredicates + +import org.springframework.http.MediaType +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.bodyValueAndAwait +import org.springframework.web.reactive.function.server.coRouter + +class RouterConfiguration { + + fun route(): RouterFunction { + // tag::snippet[] + val route = coRouter { + GET("/hello-world", accept(MediaType.TEXT_PLAIN)) { + ServerResponse.ok().bodyValueAndAwait("Hello World") + } + } + // end::snippet[] + return route + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.kt new file mode 100644 index 000000000000..c9ac0e0d424d --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnrequest + +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.http.codec.multipart.FilePartEvent +import org.springframework.http.codec.multipart.FormPartEvent +import org.springframework.http.codec.multipart.PartEvent +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.bodyToFlux +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +class PartEventHandler { + + fun handle(request: ServerRequest) { + // tag::snippet[] + request.bodyToFlux().windowUntil(PartEvent::isLast) + .concatMap { + it.switchOnFirst { signal, partEvents -> + if (signal.hasValue()) { + val event = signal.get() + if (event is FormPartEvent) { + val value: String = event.value() + // handle form field + } else if (event is FilePartEvent) { + val filename: String = event.filename() + val contents: Flux = partEvents.map(PartEvent::content) + // handle file upload + } else { + return@switchOnFirst Mono.error(RuntimeException("Unexpected event: $event")) + } + } else { + return@switchOnFirst partEvents // either complete or error signal + } + Mono.empty() + } + } + // end::snippet[] + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/RequestHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/RequestHandler.kt new file mode 100644 index 000000000000..52dfdd841c6d --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/RequestHandler.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnrequest + +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.bindAndAwait + +class RequestHandler { + + suspend fun bind(request: ServerRequest) { + // tag::snippet[] + val pet: Pet? = request.bindAndAwait{ dataBinder -> dataBinder.setAllowedFields("name") } + // end::snippet[] + } + + data class Pet(val name: String) +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.kt new file mode 100644 index 000000000000..9fd0ad27fc11 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnresponse + +import org.springframework.http.MediaType +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.bodyValueWithTypeAndAwait + +class ResponseHandler { + + suspend fun createResponse(): ServerResponse { + // tag::snippet[] + val person: Person = getPerson() + return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValueWithTypeAndAwait(person) + // end::snippet[] + } + + fun getPerson() = Person("foo") + + data class Person(val name: String) +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.kt new file mode 100644 index 000000000000..f958f21e32ce --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webfluxfnroutes + +import kotlinx.coroutines.flow.Flow +import org.springframework.docs.web.webfluxfnhandlerclasses.Person +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.coRouter + +class RouterConfiguration { + + fun routes(): RouterFunction { + // tag::snippet[] + val repository: PersonRepository = getPersonRepository() + val handler = PersonHandler(repository) + + val otherRoute: RouterFunction = getOtherRoute() + + val route = coRouter { + // GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson + GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) + // GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople + GET("/person", accept(APPLICATION_JSON), handler::listPeople) + // POST /person with no additional predicates is mapped to PersonHandler.createPerson + POST("/person", handler::createPerson) + // otherRoute is a router function that is created elsewhere and added to the route built + }.and(otherRoute) + // end::snippet[] + return route + } +} + +fun getOtherRoute() = coRouter { } + +fun getPersonRepository() = object: PersonRepository { + override fun allPeople(): Flow { + TODO("Not yet implemented") + } + + override suspend fun savePerson(person: Person) { + TODO("Not yet implemented") + } + + override suspend fun getPerson(id: Int): Person? { + TODO("Not yet implemented") + } +} 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..0577ef9edf0f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/filters/urlhandler/UrlHandlerFilterConfiguration.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index f5ee4887eb8f..c9c91dbdce9a 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index c5628f27de41..d21900a880fb 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt new file mode 100644 index 000000000000..4a315aef00be --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigapiversion + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureApiVersioning(configurer: ApiVersionConfigurer) { + configurer.useRequestHeader("API-Version") + } +} +// end::snippet[] \ No newline at end of file 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 index 50bd075660bf..5a82921a5f40 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index f77e14982ce9..81bb605cfc7e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 534fa04b8cdc..7e577969ec79 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 485b9f71e02a..ca18c988831f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 5fe920100b57..f7c52767a71e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt index c2f6d8daba16..6a3b3c8d825d 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,13 @@ * limitations under the License. */ -@file:Suppress("DEPRECATION") package org.springframework.docs.web.webmvc.mvcconfig.mvcconfiginterceptors import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor import org.springframework.web.servlet.i18n.LocaleChangeInterceptor -import org.springframework.web.servlet.theme.ThemeChangeInterceptor // tag::snippet[] @Configuration @@ -29,7 +28,7 @@ class WebConfiguration : WebMvcConfigurer { override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(LocaleChangeInterceptor()) - registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**") + registry.addInterceptor(UserRoleAuthorizationInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**") } } // end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt index 12c197a46f51..854f43ff7677 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt @@ -1,25 +1,33 @@ +@file:Suppress("DEPRECATION") + 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.http.converter.HttpMessageConverters +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter +import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import tools.jackson.databind.SerializationFeature +import tools.jackson.databind.json.JsonMapper +import tools.jackson.dataformat.xml.XmlMapper 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())) + override fun configureMessageConverters(builder: HttpMessageConverters.ServerBuilder) { + val jsonMapper = JsonMapper.builder() + .findAndAddModules() + .enable(SerializationFeature.INDENT_OUTPUT) + .defaultDateFormat(SimpleDateFormat("yyyy-MM-dd")) + .build() + val xmlMapper = XmlMapper.builder() + .findAndAddModules() + .defaultUseWrapper(false) + .build() + builder.withJsonConverter(JacksonJsonHttpMessageConverter(jsonMapper)) + .withXmlConverter(JacksonXmlHttpMessageConverter(xmlMapper)) } } // 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 index 1ee4be3095cd..077e3f0b3369 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 5cb39227e946..3d88c2bbce99 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 72c91e87f177..e63329f12d62 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..a87bc3111faa --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 6d2522c48d47..44f4c48957e6 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 23f2f5d4a267..5c69aef7671a 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 69ade9721866..7dbab09cdf77 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt index 55acaa63cb11..f572caa5cbee 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers import org.springframework.context.annotation.Bean @@ -5,14 +7,14 @@ 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 +import org.springframework.web.servlet.view.json.JacksonJsonView // tag::snippet[] @Configuration class FreeMarkerConfiguration : WebMvcConfigurer { override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.enableContentNegotiation(MappingJackson2JsonView()) + registry.enableContentNegotiation(JacksonJsonView()) registry.freeMarker().cache(false) } diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt index 472ecf25bf30..b9f6b40b8957 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,20 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") + 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 +import org.springframework.web.servlet.view.json.JacksonJsonView // tag::snippet[] @Configuration class WebConfiguration : WebMvcConfigurer { override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.enableContentNegotiation(MappingJackson2JsonView()) + registry.enableContentNegotiation(JacksonJsonView()) registry.jsp() } } 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 index 648beac23750..0123cc6dcf43 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a217953aa0fd..3a7a2ba95654 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 410b77ff06f4..6e964e00d7a6 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index f89f36d05977..2628bdbb606a 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 548ed6e56814..2843a077286c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class ExceptionController { // tag::narrow[] @ExceptionHandler(FileSystemException::class, RemoteException::class) - fun handleIoException(ex: IOException): ResponseEntity { + fun handleIOException(ex: IOException): ResponseEntity { return ResponseEntity.internalServerError().body(ex.message) } // end::narrow[] 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 index e1311de33ea3..2bacae9b68b3 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/MyConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/MyConfiguration.kt new file mode 100644 index 000000000000..c4b0f50155de --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/MyConfiguration.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcannrequestmappingregistration + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Configuration +import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.servlet.mvc.method.RequestMappingInfo +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping + +// tag::snippet[] +@Configuration +class MyConfiguration { + + // Inject the target handler and the handler mapping for controllers + @Autowired + fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { + + // Get the handler method + val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() + + // Get the handler method + val method = UserHandler::class.java.getMethod("getUser", Long::class.java) + + // Add the registration + mapping.registerMapping(info, handler, method) + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/UserHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/UserHandler.kt new file mode 100644 index 000000000000..84d71aa68c90 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcannrequestmappingregistration/UserHandler.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcannrequestmappingregistration + +class UserHandler { + + fun getUser(id: Long) { + // ... + } +} + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/AppConfig.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/AppConfig.kt new file mode 100644 index 000000000000..df7cb613faaa --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/AppConfig.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet + +import org.springframework.context.annotation.Configuration + +@Configuration +class AppConfig diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/MyWebApplicationInitializer.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/MyWebApplicationInitializer.kt new file mode 100644 index 000000000000..01d5d85a3985 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/MyWebApplicationInitializer.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet + +import jakarta.servlet.ServletContext +import org.springframework.web.WebApplicationInitializer +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext +import org.springframework.web.servlet.DispatcherServlet + +// tag::snippet[] +class MyWebApplicationInitializer : WebApplicationInitializer { + + override fun onStartup(servletContext: ServletContext) { + + // Load Spring web application configuration + val context = AnnotationConfigWebApplicationContext() + context.register(AppConfig::class.java) + + // Create and register the DispatcherServlet + val servlet = DispatcherServlet(context) + val registration = servletContext.addServlet("app", servlet) + registration.setLoadOnStartup(1) + registration.addMapping("/app/*") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcanncustomerservletcontainererrorpage/ErrorController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcanncustomerservletcontainererrorpage/ErrorController.kt new file mode 100644 index 000000000000..01707c8428de --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcanncustomerservletcontainererrorpage/ErrorController.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvcanncustomerservletcontainererrorpage + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +// tag::snippet[] +@RestController +class ErrorController { + + @RequestMapping(path = ["/error"]) + fun handle(request: HttpServletRequest): Map { + val map = HashMap() + map["status"] = request.getAttribute("jakarta.servlet.error.status_code")!! + map["reason"] = request.getAttribute("jakarta.servlet.error.message")!! + return map + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyFilterDispatcherServletInitializer.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyFilterDispatcherServletInitializer.kt new file mode 100644 index 000000000000..938922f82a81 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyFilterDispatcherServletInitializer.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvccontainerconfig + +import jakarta.servlet.Filter +import org.springframework.web.context.WebApplicationContext +import org.springframework.web.filter.CharacterEncodingFilter +import org.springframework.web.filter.HiddenHttpMethodFilter +import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer + +// tag::snippet[] +class MyFilterDispatcherServletInitializer : AbstractDispatcherServletInitializer() { + + override fun getServletFilters(): Array { + return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter()) + } + + // @fold:on + override fun createServletApplicationContext(): WebApplicationContext { + /**/TODO("Not yet implemented") + } + + override fun getServletMappings(): Array { + /**/TODO("Not yet implemented") + } + + override fun createRootApplicationContext(): WebApplicationContext? { + /**/TODO("Not yet implemented") + } + // @fold:off +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebAppInitializer.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebAppInitializer.kt new file mode 100644 index 000000000000..466deab6d962 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebAppInitializer.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvccontainerconfig + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer + +// tag::snippet[] +class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { + + override fun getRootConfigClasses(): Array>? { + return null + } + + override fun getServletConfigClasses(): Array>? { + return arrayOf(MyWebConfig::class.java) + } + + override fun getServletMappings(): Array { + return arrayOf("/") + } +} +// end::snippet[] + +class MyWebConfig diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebApplicationInitializer.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebApplicationInitializer.kt new file mode 100644 index 000000000000..2b09911c8430 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyWebApplicationInitializer.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvccontainerconfig + +import jakarta.servlet.ServletContext +import org.springframework.web.WebApplicationInitializer +import org.springframework.web.context.support.XmlWebApplicationContext +import org.springframework.web.servlet.DispatcherServlet + +// tag::snippet[] +class MyWebApplicationInitializer : WebApplicationInitializer { + + override fun onStartup(container: ServletContext) { + val appContext = XmlWebApplicationContext() + appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml") + + val registration = container.addServlet("dispatcher", DispatcherServlet(appContext)) + registration.setLoadOnStartup(1) + registration.addMapping("/") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyXmlDispatcherServletInitializer.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyXmlDispatcherServletInitializer.kt new file mode 100644 index 000000000000..815b9909e565 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvccontainerconfig/MyXmlDispatcherServletInitializer.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvccontainerconfig + +import org.springframework.web.context.WebApplicationContext +import org.springframework.web.context.support.XmlWebApplicationContext +import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer + +// tag::snippet[] +class MyXmlDispatcherServletInitializer : AbstractDispatcherServletInitializer() { + + override fun createRootApplicationContext(): WebApplicationContext? { + return null + } + + override fun createServletApplicationContext(): WebApplicationContext { + return XmlWebApplicationContext().apply { + setConfigLocation("/WEB-INF/spring/dispatcher-config.xml") + } + } + + override fun getServletMappings(): Array { + return arrayOf("/") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolvercookie/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolvercookie/WebConfiguration.kt new file mode 100644 index 000000000000..3dcb877621cf --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolvercookie/WebConfiguration.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvclocaleresolvercookie + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.LocaleResolver +import org.springframework.web.servlet.i18n.CookieLocaleResolver +import java.time.Duration + +// tag::snippet[] +@Configuration +class WebConfiguration { + + @Bean + fun localeResolver(): LocaleResolver = CookieLocaleResolver("clientlanguage").apply { + setCookieMaxAge(Duration.ofSeconds(100000)) + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolverinterceptor/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolverinterceptor/WebConfiguration.kt new file mode 100644 index 000000000000..f42488c03101 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolverinterceptor/WebConfiguration.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvclocaleresolverinterceptor + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.LocaleResolver +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping +import org.springframework.web.servlet.i18n.CookieLocaleResolver +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor + +// tag::snippet[] +@Configuration +class WebConfiguration { + + @Bean + fun localeResolver(): LocaleResolver { + return CookieLocaleResolver() + } + + @Bean + fun urlMapping() = SimpleUrlHandlerMapping().apply { + setInterceptors(LocaleChangeInterceptor().apply { + paramName = "siteLanguage" + }) + urlMap = mapOf("/**/*.view" to "someController") + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcloggingsensitivedata/MyInitializer.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcloggingsensitivedata/MyInitializer.kt new file mode 100644 index 000000000000..f06d952e9a9b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcloggingsensitivedata/MyInitializer.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvcloggingsensitivedata + +import jakarta.servlet.ServletRegistration + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer + +// tag::snippet[] +class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { + + // @fold:on + override fun getRootConfigClasses(): Array>? { + /**/TODO("Not yet implemented") + } + + override fun getServletConfigClasses(): Array>? { + /**/TODO("Not yet implemented") + } + + override fun getServletMappings(): Array { + /**/TODO("Not yet implemented") + } + + // @fold:off + override fun customizeRegistration(registration: ServletRegistration.Dynamic) { + registration.setInitParameter("enableLoggingRequestDetails", "true") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcmultipartresolverstandard/AppInitializer.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcmultipartresolverstandard/AppInitializer.kt new file mode 100644 index 000000000000..6685776cc2f4 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcmultipartresolverstandard/AppInitializer.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvcmultipartresolverstandard + +import jakarta.servlet.MultipartConfigElement +import jakarta.servlet.ServletRegistration + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer + +// tag::snippet[] +class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { + + // @fold:on + override fun getServletMappings(): Array { + /**/TODO("Not yet implemented") + } + + override fun getRootConfigClasses(): Array>? { + /**/TODO("Not yet implemented") + } + + override fun getServletConfigClasses(): Array>? { + /**/TODO("Not yet implemented") + } + + // @fold:off + override fun customizeRegistration(registration: ServletRegistration.Dynamic) { + + // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold + registration.setMultipartConfig(MultipartConfigElement("/tmp")) + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcservletcontexthierarchy/MyWebAppInitializer.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcservletcontexthierarchy/MyWebAppInitializer.kt new file mode 100644 index 000000000000..e826d324f2c2 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcservlet/mvcservletcontexthierarchy/MyWebAppInitializer.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mvcservlet.mvcservletcontexthierarchy + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer + +// tag::snippet[] +class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { + + override fun getRootConfigClasses(): Array> { + return arrayOf(RootConfig::class.java) + } + + override fun getServletConfigClasses(): Array> { + return arrayOf(App1Config::class.java) + } + + override fun getServletMappings(): Array { + return arrayOf("/app1/*") + } +} +// end::snippet[] + +class RootConfig +class App1Config diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcfnrunning/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcfnrunning/WebConfiguration.kt new file mode 100644 index 000000000000..c0d1d0cb57b6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcfnrunning/WebConfiguration.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcfnrunning + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.converter.HttpMessageConverters +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.function.RouterFunction + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + @Bean + fun routerFunctionA(): RouterFunction<*> { + TODO() + } + + @Bean + fun routerFunctionB(): RouterFunction<*> { + TODO() + } + + override fun configureMessageConverters(builder: HttpMessageConverters.ServerBuilder) { + TODO() + } + + override fun addCorsMappings(registry: CorsRegistry) { + TODO() + } + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + TODO() + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewfreemarkercontextconfig/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewfreemarkercontextconfig/WebConfiguration.kt new file mode 100644 index 000000000000..96728a34421f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewfreemarkercontextconfig/WebConfiguration.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewfreemarkercontextconfig + +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 java.nio.charset.StandardCharsets + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.freeMarker() + } + + // Configure FreeMarker... + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("/WEB-INF/freemarker") + setDefaultCharset(StandardCharsets.UTF_8) + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewgroovymarkupconfiguration/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewgroovymarkupconfiguration/WebConfiguration.kt new file mode 100644 index 000000000000..5e990aafbfb4 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewgroovymarkupconfiguration/WebConfiguration.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewgroovymarkupconfiguration + +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.groovy.GroovyMarkupConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.groovy() + } + + // Configure the Groovy Markup Template Engine... + + @Bean + fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply { + resourceLoaderPath = "/WEB-INF/" + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewjspresolver/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewjspresolver/WebConfiguration.kt new file mode 100644 index 000000000000..0fd338e3c92a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewjspresolver/WebConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewjspresolver + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.jsp() + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewscriptintegrate/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewscriptintegrate/WebConfiguration.kt new file mode 100644 index 000000000000..42c2309d122a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewscriptintegrate/WebConfiguration.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewscriptintegrate + +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.script.ScriptTemplateConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.scriptTemplate() + } + + @Bean + fun configurer() = ScriptTemplateConfigurer().apply { + engineName = "jython" + setScripts("render.py") + renderFunction = "render" + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewsfreemarker/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewsfreemarker/WebConfiguration.kt new file mode 100644 index 000000000000..e41dd27b68e7 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewsfreemarker/WebConfiguration.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewsfreemarker + +import freemarker.template.utility.XmlEscape +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer + +@Configuration +class WebConfiguration { + + // tag::snippet[] + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("/WEB-INF/freemarker") + setFreemarkerVariables(mapOf("xml_escape" to XmlEscape())) + } + // end::snippet[] +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewxsltbeandefs/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewxsltbeandefs/WebConfiguration.kt new file mode 100644 index 000000000000..57d4dc65af3b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvcview/mvcviewxsltbeandefs/WebConfiguration.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.webmvcview.mvcviewxsltbeandefs + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.view.xslt.XsltViewResolver + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + @Bean + fun xsltViewResolver() = XsltViewResolver().apply { + setPrefix("/WEB-INF/xsl/") + setSuffix(".xslt") + } +} +// end::snippet[] 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 index d03e7e0995d4..91c876eb7efa 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index b8affe8c5ee0..4199da12ef4c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index ecdd126a33ff..66170ae957ec 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 88ebf88248fa..5d0197867f00 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 0e0502a6501c..923e602a8508 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 554d70962155..8c68191ca2f6 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index aa3630b5c693..e5f2113a10ca 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 1fb61d28b8bb..f75e686034e9 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 4602aaee2089..4b0df93edc65 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 45c7f93e1301..43d07c0f0a4f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 2516b8d73018..cbc83df78244 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index e464312da142..5f6f5d48373c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 74c6d508e6bf..a2db5f686315 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index a5324bbe0b2c..d89cdf530ae5 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 75420c84ac44..3c9c04ff84ab 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 50b6916fe866..7345309b05e0 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketfallbacksockjsclient/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketfallbacksockjsclient/WebSocketConfiguration.kt new file mode 100644 index 000000000000..d89b6cc38060 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketfallbacksockjsclient/WebSocketConfiguration.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.websocketfallbacksockjsclient + +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.config.annotation.StompEndpointRegistry +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurationSupport + +// tag::snippet[] +@Configuration +class WebSocketConfiguration : WebSocketMessageBrokerConfigurationSupport() { + + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + registry.addEndpoint("/sockjs").withSockJS() + // Set the streamBytesLimit property to 512KB (the default is 128KB -- 128 * 1024) + .setStreamBytesLimit(512 * 1024) + // Set the httpMessageCacheSize property to 1,000 (the default is 100) + .setHttpMessageCacheSize(1000) + // Set the disconnectDelay property to 30 property seconds (the default is five seconds -- 5 * 1000) + .setDisconnectDelay(30 * 1000) + } + + // ... +} +// 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 index 432e8044882f..4e93c0dcbdc1 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketfallbackxhrvsiframe/WebSocketConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketfallbackxhrvsiframe/WebSocketConfiguration.kt new file mode 100644 index 000000000000..c8b86e91ac22 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketfallbackxhrvsiframe/WebSocketConfiguration.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.websocketfallbackxhrvsiframe + +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() + .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js") + } + + // ... + + override fun configureMessageBroker(registry: MessageBrokerRegistry) { + // Configure message broker... + } +} +// 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 index 79f80e690df1..71d02dc929ec 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 38b53e01b646..7892a9776c1d 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 5217d325d165..f72c7d25ca85 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index fd6d5fd1c62f..1ae0755a988f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 index 0c2faf491a8f..a2de242be40b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..38eee51e91e2 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/MyEchoHandler.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 index 32f4357a3eea..9d7de774511e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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/transaction/declarative/transactiondeclarativeannotations/AppConfig.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/AppConfig.xml new file mode 100644 index 000000000000..55230b90c9c8 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/transaction/declarative/transactiondeclarativeannotations/AppConfig.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + 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 index a0331c308966..96fc1be4e904 100644 --- 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 @@ -15,4 +15,13 @@ + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/mvccorsglobal/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/mvccorsglobal/WebConfiguration.xml new file mode 100644 index 000000000000..66244af74ef1 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/mvccorsglobal/WebConfiguration.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml index 2d0f1cae1ead..51f91158b468 100644 --- a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml @@ -14,7 +14,9 @@ - + + + diff --git a/framework-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 deleted file mode 100644 index f63d2aab1ff3..000000000000 --- a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file 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 index d019b5533535..3b142562f493 100644 --- 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 @@ -12,7 +12,7 @@ - + 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 index f6dba12f1f1f..79df75244e15 100644 --- 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 @@ -12,7 +12,7 @@ - + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/MyWebApplicationInitializer.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/MyWebApplicationInitializer.xml new file mode 100644 index 000000000000..47ea2fd3e3a9 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/MyWebApplicationInitializer.xml @@ -0,0 +1,29 @@ + + + + + org.springframework.web.context.ContextLoaderListener + + + + contextConfigLocation + /WEB-INF/app-context.xml + + + + app + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + + + 1 + + + + app + /app/* + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolvercookie/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolvercookie/WebConfiguration.xml new file mode 100644 index 000000000000..23c624bcfc6a --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolvercookie/WebConfiguration.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolverinterceptor/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolverinterceptor/WebConfiguration.xml new file mode 100644 index 000000000000..6f659cb03005 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/mvclocaleresolverinterceptor/WebConfiguration.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + /**/*.view=someController + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/mvcservletcontexthierarchy/MyWebAppInitializer.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/mvcservletcontexthierarchy/MyWebAppInitializer.xml new file mode 100644 index 000000000000..7db5049c3ef2 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcservlet/mvcservletcontexthierarchy/MyWebAppInitializer.xml @@ -0,0 +1,29 @@ + + + + + org.springframework.web.context.ContextLoaderListener + + + + contextConfigLocation + /WEB-INF/root-context.xml + + + + app1 + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + /WEB-INF/app1-context.xml + + 1 + + + + app1 + /app1/* + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewfreemarkercontextconfig/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewfreemarkercontextconfig/WebConfiguration.xml new file mode 100644 index 000000000000..6b0c42cb0537 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewfreemarkercontextconfig/WebConfiguration.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewgroovymarkupconfiguration/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewgroovymarkupconfiguration/WebConfiguration.xml new file mode 100644 index 000000000000..50a9d3f03c3a --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewgroovymarkupconfiguration/WebConfiguration.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewjspresolver/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewjspresolver/WebConfiguration.xml new file mode 100644 index 000000000000..be42ac149d64 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewjspresolver/WebConfiguration.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewscriptintegrate/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewscriptintegrate/WebConfiguration.xml new file mode 100644 index 000000000000..a2e003a7bbc4 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewscriptintegrate/WebConfiguration.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewsfreemarker/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewsfreemarker/WebConfiguration.xml new file mode 100644 index 000000000000..09ab5fb3e0fb --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvcview/mvcviewsfreemarker/WebConfiguration.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 81cdd0773de5..8a72bad61c31 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -7,145 +7,140 @@ javaPlatform { } dependencies { - api(platform("com.fasterxml.jackson:jackson-bom:2.15.4")) - api(platform("io.micrometer:micrometer-bom:1.13.0")) - api(platform("io.netty:netty-bom:4.1.109.Final")) - api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2024.0.0-M2")) - api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.21")) - api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) - api(platform("org.assertj:assertj-bom:3.26.0")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.9")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.9")) - 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.2")) - api(platform("org.mockito:mockito-bom:5.12.0")) + api(platform("com.fasterxml.jackson:jackson-bom:2.21.5")) + api(platform("io.micrometer:micrometer-bom:1.18.0-SNAPSHOT")) + api(platform("io.netty:netty-bom:4.2.17.Final")) + api(platform("io.projectreactor:reactor-bom:2026.0.0-SNAPSHOT")) + api(platform("io.rsocket:rsocket-bom:1.1.5")) + api(platform("org.apache.groovy:groovy-bom:5.0.8")) + api(platform("org.apache.logging.log4j:log4j-bom:2.26.1")) + api(platform("org.assertj:assertj-bom:3.27.7")) + api(platform("org.eclipse.jetty:jetty-bom:12.1.12")) + api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.12")) + api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0")) + api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0")) + api(platform("org.junit:junit-bom:6.1.2")) + api(platform("org.mockito:mockito-bom:5.23.0")) + api(platform("tools.jackson:jackson-bom:3.1.5")) constraints { - api("com.fasterxml:aalto-xml:1.3.2") - api("com.fasterxml.woodstox:woodstox-core:6.6.2") - api("com.github.ben-manes.caffeine:caffeine:3.1.8") + api("com.fasterxml:aalto-xml:1.4.0") + api("com.fasterxml.woodstox:woodstox-core:7.2.1") + api("com.github.ben-manes.caffeine:caffeine:3.2.4") 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.25.3") - api("com.h2database:h2:2.2.224") - api("com.jayway.jsonpath:json-path:2.9.0") + api("com.google.code.gson:gson:2.14.0") + api("com.google.protobuf:protobuf-java-util:4.35.1") + api("com.h2database:h2:2.4.240") + api("com.jayway.jsonpath:json-path:2.10.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.squareup.okhttp3:mockwebserver3:5.3.0") 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.1.0") - api("com.thoughtworks.xstream:xstream:1.4.20") - api("commons-io:commons-io:2.15.0") + api("com.thoughtworks.qdox:qdox:2.2.0") + api("com.thoughtworks.xstream:xstream:1.4.21") + api("commons-io:commons-io:2.21.0") + api("commons-logging:commons-logging:1.3.5") api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2") - api("io.micrometer:context-propagation:1.1.1") - api("io.mockk:mockk:1.13.4") - api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3") + api("io.mockk:mockk:1.14.5") api("io.projectreactor.tools:blockhound:1.0.8.RELEASE") - api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") + api("io.r2dbc:r2dbc-h2:1.1.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.8") + api("io.reactivex.rxjava3:rxjava:3.1.12") api("io.smallrye.reactive:mutiny:1.10.0") - api("io.undertow:undertow-core:2.3.13.Final") - api("io.undertow:undertow-servlet:2.3.13.Final") - api("io.undertow:undertow-websockets-jsr:2.3.13.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("io.vavr:vavr:0.11.0") + api("jakarta.activation:jakarta.activation-api:2.1.3") + api("jakarta.annotation:jakarta.annotation-api:3.0.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("org.apache-extras.beanshell:bsh:2.0b6") - 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.activemq:activemq-broker:5.17.7") + api("org.apache.activemq:activemq-kahadb-store:5.17.7") + api("org.apache.activemq:activemq-stomp:5.17.7") + api("org.apache.activemq:artemis-jakarta-client:2.42.0") + api("org.apache.activemq:artemis-junit-5:2.42.0") + api("org.apache.commons:commons-pool2:2.12.1") 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.3.1") - api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.4") - api("org.apache.poi:poi-ooxml:5.2.5") - api("org.apache.tomcat.embed:tomcat-embed-core:10.1.24") - api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.24") - api("org.apache.tomcat:tomcat-util:10.1.24") - api("org.apache.tomcat:tomcat-websocket:10.1.24") - api("org.aspectj:aspectjrt:1.9.22.1") - api("org.aspectj:aspectjtools:1.9.22.1") - api("org.aspectj:aspectjweaver:1.9.22.1") - api("org.awaitility:awaitility:4.2.0") + api("org.apache.httpcomponents.client5:httpclient5:5.6") + api("org.apache.httpcomponents.core5:httpcore5-reactive:5.4.2") + api("org.apache.poi:poi-ooxml:5.5.1") + api("org.apache.tomcat.embed:tomcat-embed-core:11.0.24") + api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.24") + api("org.apache.tomcat:tomcat-util:11.0.24") + api("org.apache.tomcat:tomcat-websocket:11.0.24") + api("org.aspectj:aspectjrt:1.9.25") + api("org.aspectj:aspectjtools:1.9.25") + api("org.aspectj:aspectjweaver:1.9.25") + 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.4") - api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4") - api("org.eclipse:yasson:2.0.4") + api("org.dom4j:dom4j:2.2.0") + api("org.easymock:easymock:5.6.0") + api("org.eclipse.angus:angus-mail:2.0.3") + api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.5") + api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.1") + 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") + api("org.freemarker:freemarker:2.3.34") api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea") api("org.glassfish:jakarta.el:4.0.2") - api("org.glassfish.tyrus:tyrus-container-servlet:2.1.3") api("org.graalvm.sdk:graal-sdk:22.3.1") - api("org.hamcrest:hamcrest:2.2") - api("org.hibernate:hibernate-core-jakarta:5.6.15.Final") - api("org.hibernate:hibernate-validator:7.0.5.Final") - api("org.hsqldb:hsqldb:2.7.2") - api("org.htmlunit:htmlunit:4.1.0") - api("org.javamoney:moneta:1.4.2") - api("org.jruby:jruby:9.4.6.0") - api("org.junit.support:testng-engine:1.0.5") - api("org.mozilla:rhino:1.7.14") + api("org.hamcrest:hamcrest:3.0") + api("org.hibernate.orm:hibernate-core:7.4.5.Final") + api("org.hibernate.validator:hibernate-validator:9.1.3.Final") + api("org.hsqldb:hsqldb:2.7.4") + api("org.htmlunit:htmlunit:4.21.0") + api("org.javamoney:moneta:1.4.4") + api("org.jboss.logging:jboss-logging:3.6.1.Final") + api("org.jruby:jruby:10.0.2.0") + api("org.jspecify:jspecify:1.0.0") + api("org.junit.support:testng-engine:1.1.0") + 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:htmlunit3-driver:4.20.0") - api("org.seleniumhq.selenium:selenium-java:4.20.0") - api("org.skyscreamer:jsonassert:1.5.1") - api("org.slf4j:slf4j-api:2.0.12") - api("org.testng:testng:7.9.0") - api("org.webjars:underscorejs:1.8.3") - api("org.webjars:webjars-locator-core:0.55") - api("org.webjars:webjars-locator-lite:1.0.0") - api("org.xmlunit:xmlunit-assertj:2.9.1") - api("org.xmlunit:xmlunit-matchers:2.9.1") - api("org.yaml:snakeyaml:2.2") + api("org.reactivestreams:reactive-streams:1.0.4") + api("org.seleniumhq.selenium:htmlunit3-driver:4.41.0") + api("org.seleniumhq.selenium:selenium-java:4.41.0") + api("org.skyscreamer:jsonassert:1.5.3") + api("org.testng:testng:7.12.0") + api("org.webjars:webjars-locator-lite:1.1.0") + api("org.xmlunit:xmlunit-assertj:2.10.4") + api("org.xmlunit:xmlunit-matchers:2.10.4") + api("org.yaml:snakeyaml:2.6") } } diff --git a/gradle.properties b/gradle.properties index 6af60d911ede..20f44e0eadc2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,11 @@ -version=6.2.0-SNAPSHOT +version=7.1.0-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m org.gradle.parallel=true -kotlinVersion=1.9.22 +kotlinVersion=2.4.10 +byteBuddyVersion=1.17.6 kotlin.jvm.target.validation.mode=ignore kotlin.stdlib.default.dependency=false diff --git a/gradle/docs-dokka.gradle b/gradle/docs-dokka.gradle deleted file mode 100644 index 7d593bf49de1..000000000000 --- a/gradle/docs-dokka.gradle +++ /dev/null @@ -1,32 +0,0 @@ -tasks.findByName("dokkaHtmlPartial")?.configure { - outputDirectory.set(new File(buildDir, "docs/kdoc")) - dokkaSourceSets { - configureEach { - sourceRoots.setFrom(file("src/main/kotlin")) - 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/")) - } - externalDocumentationLink { - url.set(new URL("https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/")) - } - externalDocumentationLink { - url.set(new URL("https://kotlin.github.io/kotlinx.coroutines/")) - } - externalDocumentationLink { - url.set(new URL("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/")) - } - 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..a6d561e76b3c 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -7,7 +7,6 @@ apply plugin: 'eclipse' eclipse.jdt { sourceCompatibility = 17 targetCompatibility = 17 - javaRuntimeName = "JavaSE-17" } // Replace classpath entries with project dependencies (GRADLE-1116) @@ -69,6 +68,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 { @@ -78,7 +84,7 @@ eclipse.classpath.file.whenMerged { } // 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 +93,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..51fbd0b3f21e 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 { @@ -61,4 +61,4 @@ void configureDeploymentRepository(Project project) { } } } -} \ No newline at end of file +} diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index e41e81f83a72..1a4ff436aa61 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -3,23 +3,16 @@ 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' +apply plugin: "io.spring.nullability" dependencies { jmh 'org.openjdk.jmh:jmh-core:1.37' jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.37' jmh 'net.sf.jopt-simple:jopt-simple' - errorprone 'com.uber.nullaway:nullaway:0.10.26' - errorprone 'com.google.errorprone:error_prone_core:2.9.0' -} - -pluginManager.withPlugin("kotlin") { - apply plugin: "org.jetbrains.dokka" - apply from: "${rootDir}/gradle/docs-dokka.gradle" } jmh { @@ -69,32 +62,47 @@ normalization { javadoc { description = "Generates project-level javadoc for use in -javadoc jar" + failOnError = true + options { + encoding = "UTF-8" + memberLevel = JavadocMemberLevel.PROTECTED + author = true + header = project.name + use = true + links(project.ext.javadocLinks) + setOutputLevel(JavadocOutputLevel.QUIET) + // Check for 'syntax' during linting. Note that the global + // 'framework-api:javadoc' task checks for 'reference' in addition + // to 'syntax'. + addBooleanOption("Xdoclint:syntax,-reference", true) + // Change modularity mismatch from warn to info. + // See https://github.com/spring-projects/spring-framework/issues/27497 + addStringOption("-link-modularity-mismatch", "info") + // With the javadoc tool on Java 25, it appears that the 'reference' + // group is always active and the '-reference' flag is not honored. + // Thus, we do NOT fail the build on Javadoc warnings due to + // cross-module @see and @link references which are only reachable + // when running the global 'framework-api:javadoc' task. + addBooleanOption('Werror', false) + // do not ship 4MB of web fonts for single modules + addBooleanOption("-no-fonts", true) + } - options.encoding = "UTF-8" - options.memberLevel = JavadocMemberLevel.PROTECTED - options.author = true - options.header = project.name - options.use = true - options.links(project.ext.javadocLinks) - // Check for syntax during linting. 'none' doesn't seem to work in suppressing - // all linting warnings all the time (see/link references most notably). - options.addStringOption("Xdoclint:syntax", "-quiet") - - // Suppress warnings due to cross-module @see and @link references. - // Note that global 'api' task does display all warnings, and - // checks for 'reference' on top of 'syntax'. + // Attempt to suppress warnings due to cross-module @see and @link references. + // Note that the global 'framework-api:javadoc' task displays all warnings. logging.captureStandardError LogLevel.INFO logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message } -task sourcesJar(type: Jar, dependsOn: classes) { +tasks.register('sourcesJar', Jar) { + dependsOn classes duplicatesStrategy = DuplicatesStrategy.EXCLUDE archiveClassifier.set("sources") from sourceSets.main.allSource // Don't include or exclude anything explicitly by default. See SPR-12085. } -task javadocJar(type: Jar) { +tasks.register('javadocJar', Jar) { archiveClassifier.set("javadoc") from javadoc } @@ -112,18 +120,3 @@ 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:CustomContractAnnotations", "org.springframework.lang.Contract") - option("NullAway:AnnotatedPackages", "org.springframework") - option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + - "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + - "org.springframework.javapoet,org.springframework.aot.nativex.substitution,org.springframework.aot.nativex.feature") - } -} -tasks.compileJava { - // The check defaults to a warning, bump it up to an error for the main sources - options.errorprone.error("NullAway") -} \ No newline at end of file diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle deleted file mode 100644 index 152abb08db45..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.develocity.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") - resolvedMainToolchain = true - } - if (testToolchainConfigured() && !resolvedTestToolchain && task instanceof Test && task.javaLauncher.isPresent()) { - def metadata = task.javaLauncher.get().metadata - task.project.develocity.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") - resolvedTestToolchain = true - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4..eddabd2eef8d 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 b82aa23a4f05..69dd0d0404a8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1aa94a426907..249efbb032ce 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,10 +15,12 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -27,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -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/3d91ce3b8caaf77ad09f381f43615b715b53f72c/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 @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -203,15 +203,14 @@ 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. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 7101f8e4676f..8508ef684d4e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,16 +13,18 @@ @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 ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -49,7 +51,7 @@ 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 +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -63,30 +65,18 @@ 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 +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% -:mainEnd -if "%OS%"=="Windows_NT" endlocal +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/import-into-eclipse.md b/import-into-eclipse.md index 9258de40dba5..4754d61aacac 100644 --- a/import-into-eclipse.md +++ b/import-into-eclipse.md @@ -54,4 +54,4 @@ _When instructed to execute `./gradlew` from the command line, be sure to execut In any case, please do not check in your own generated `.classpath` file, `.project` file, or `.settings` folder. You'll notice these files are already intentionally in -`.gitignore`. The same policy holds for IDEA metadata. +`.gitignore`. The same policy holds for IntelliJ IDEA metadata. diff --git a/import-into-idea.md b/import-into-intellij-idea.md similarity index 100% rename from import-into-idea.md rename to import-into-intellij-idea.md 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/AopNamespaceHandlerAdviceOrderIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java index 8cc9a4c05407..73bce9a78b1a 100644 --- a/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java index c711d6e4e75a..c53e02da56d8 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,10 +61,9 @@ class AopNamespaceHandlerScopeIntegrationTests { @Test - void testSingletonScoping() throws Exception { + void singletonScoping() throws Exception { assertThat(AopUtils.isAopProxy(singletonScoped)).as("Should be AOP proxy").isTrue(); - boolean condition = singletonScoped instanceof TestBean; - assertThat(condition).as("Should be target class proxy").isTrue(); + assertThat(singletonScoped).as("Should be target class proxy").isInstanceOf(TestBean.class); String rob = "Rob Harrop"; String bram = "Bram Smeets"; assertThat(singletonScoped.getName()).isEqualTo(rob); @@ -75,19 +74,17 @@ void testSingletonScoping() throws Exception { } @Test - void testRequestScoping() { + void requestScoping() { MockHttpServletRequest oldRequest = new MockHttpServletRequest(); MockHttpServletRequest newRequest = new MockHttpServletRequest(); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(oldRequest)); assertThat(AopUtils.isAopProxy(requestScoped)).as("Should be AOP proxy").isTrue(); - boolean condition = requestScoped instanceof TestBean; - assertThat(condition).as("Should be target class proxy").isTrue(); + assertThat(requestScoped).as("Should be target class proxy").isInstanceOf(TestBean.class); assertThat(AopUtils.isAopProxy(testBean)).as("Should be AOP proxy").isTrue(); - boolean condition1 = testBean instanceof TestBean; - assertThat(condition1).as("Regular bean should be JDK proxy").isFalse(); + assertThat(testBean).as("Regular bean should be JDK proxy").isNotInstanceOf(TestBean.class); String rob = "Rob Harrop"; String bram = "Bram Smeets"; @@ -103,7 +100,7 @@ void testRequestScoping() { } @Test - void testSessionScoping() { + void sessionScoping() { MockHttpSession oldSession = new MockHttpSession(); MockHttpSession newSession = new MockHttpSession(); @@ -112,14 +109,12 @@ void testSessionScoping() { RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); assertThat(AopUtils.isAopProxy(sessionScoped)).as("Should be AOP proxy").isTrue(); - boolean condition1 = sessionScoped instanceof TestBean; - assertThat(condition1).as("Should not be target class proxy").isFalse(); + assertThat(sessionScoped).as("Should not be target class proxy").isNotInstanceOf(TestBean.class); assertThat(sessionScopedAlias).isSameAs(sessionScoped); assertThat(AopUtils.isAopProxy(testBean)).as("Should be AOP proxy").isTrue(); - boolean condition = testBean instanceof TestBean; - assertThat(condition).as("Regular bean should be JDK proxy").isFalse(); + assertThat(testBean).as("Regular bean should be JDK proxy").isNotInstanceOf(TestBean.class); String rob = "Rob Harrop"; String bram = "Bram Smeets"; diff --git a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java index 15fbd10d787a..fa6757fc621d 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.List; import jakarta.servlet.ServletException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.support.AopUtils; @@ -31,7 +32,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.transaction.NoTransactionException; import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.transaction.testfixture.CallCountingTransactionManager; @@ -65,7 +65,7 @@ protected BeanFactory getBeanFactory() { } @Test - void testDefaultExclusionPrefix() { + void defaultExclusionPrefix() { DefaultAdvisorAutoProxyCreator aapc = (DefaultAdvisorAutoProxyCreator) getBeanFactory().getBean(ADVISOR_APC_BEAN_NAME); assertThat(aapc.getAdvisorBeanNamePrefix()).isEqualTo((ADVISOR_APC_BEAN_NAME + DefaultAdvisorAutoProxyCreator.SEPARATOR)); assertThat(aapc.isUsePrefix()).isFalse(); @@ -75,21 +75,21 @@ void testDefaultExclusionPrefix() { * If no pointcuts match (no attrs) there should be proxying. */ @Test - void testNoProxy() { + void noProxy() { BeanFactory bf = getBeanFactory(); Object o = bf.getBean("noSetters"); assertThat(AopUtils.isAopProxy(o)).isFalse(); } @Test - void testTxIsProxied() { + void txIsProxied() { BeanFactory bf = getBeanFactory(); ITestBean test = (ITestBean) bf.getBean("test"); assertThat(AopUtils.isAopProxy(test)).isTrue(); } @Test - void testRegexpApplied() { + void regexpApplied() { BeanFactory bf = getBeanFactory(); ITestBean test = (ITestBean) bf.getBean("test"); MethodCounter counter = (MethodCounter) bf.getBean("countingAdvice"); @@ -99,7 +99,7 @@ void testRegexpApplied() { } @Test - void testTransactionAttributeOnMethod() { + void transactionAttributeOnMethod() { BeanFactory bf = getBeanFactory(); ITestBean test = (ITestBean) bf.getBean("test"); @@ -121,7 +121,7 @@ void testTransactionAttributeOnMethod() { * Should not roll back on servlet exception. */ @Test - void testRollbackRulesOnMethodCauseRollback() throws Exception { + void rollbackRulesOnMethodCauseRollback() throws Exception { BeanFactory bf = getBeanFactory(); Rollback rb = (Rollback) bf.getBean("rollback"); @@ -147,7 +147,7 @@ void testRollbackRulesOnMethodCauseRollback() throws Exception { } @Test - void testRollbackRulesOnMethodPreventRollback() throws Exception { + void rollbackRulesOnMethodPreventRollback() throws Exception { BeanFactory bf = getBeanFactory(); Rollback rb = (Rollback) bf.getBean("rollback"); @@ -158,19 +158,17 @@ void testRollbackRulesOnMethodPreventRollback() throws Exception { try { rb.echoException(new ServletException()); } - catch (ServletException ex) { - + catch (ServletException ignored) { } assertThat(txMan.commits).as("Transaction counts match").isEqualTo(1); } @Test - void testProgrammaticRollback() { + void programmaticRollback() { BeanFactory bf = getBeanFactory(); Object bean = bf.getBean(TXMANAGER_BEAN_NAME); - boolean condition = bean instanceof CallCountingTransactionManager; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(CallCountingTransactionManager.class); CallCountingTransactionManager txMan = (CallCountingTransactionManager) bf.getBean(TXMANAGER_BEAN_NAME); Rollback rb = (Rollback) bf.getBean("rollback"); @@ -272,7 +270,7 @@ public void before(Method method, Object[] args, Object target) throws Throwable TransactionInterceptor.currentTransactionStatus(); throw new RuntimeException("Shouldn't have a transaction"); } - catch (NoTransactionException ex) { + catch (NoTransactionException ignored) { // this is Ok } } diff --git a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java index d09a4cf78117..b645ebd6423f 100644 --- a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java index 1de66c6612a9..b00b8f68086b 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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..dc43736d9cfe 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-present the original author 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/aot/test/SampleReflection.java b/integration-tests/src/test/java/org/springframework/aot/test/SampleReflection.java index 3f8cbadffc61..fb87efabd59c 100644 --- a/integration-tests/src/test/java/org/springframework/aot/test/SampleReflection.java +++ b/integration-tests/src/test/java/org/springframework/aot/test/SampleReflection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/Component.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/Component.java index aeb34d25c0fe..6f29fdb5c61f 100644 --- a/integration-tests/src/test/java/org/springframework/beans/factory/xml/Component.java +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/Component.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParser.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParser.java index 833c856adfb8..de94cd90a01e 100644 --- a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParser.java +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 613fcb32e9d2..8f09735cc1b7 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,13 +50,13 @@ void tearDown() { } @Test - void testBionicBasic() { + void bionicBasic() { Component cp = getBionicFamily(); assertThat(cp.getName()).isEqualTo("Bionic-1"); } @Test - void testBionicFirstLevelChildren() { + void bionicFirstLevelChildren() { Component cp = getBionicFamily(); List components = cp.getComponents(); assertThat(components).hasSize(2); @@ -65,7 +65,7 @@ void testBionicFirstLevelChildren() { } @Test - void testBionicSecondLevelChildren() { + void bionicSecondLevelChildren() { Component cp = getBionicFamily(); List components = cp.getComponents().get(0).getComponents(); assertThat(components).hasSize(2); 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 12255e838860..7e5566ccc735 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentNamespaceHandler.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentNamespaceHandler.java index abd9867c0c4e..4a0da653bed7 100644 --- a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentNamespaceHandler.java +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java index 104ca11ca1e4..87110c550672 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java index adce710f1606..ac35c46bcc01 100644 --- a/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,7 +83,7 @@ void reset() { @Test - void testPrototype() { + void prototype() { ApplicationContext context = createContext(ScopedProxyMode.NO); ScopedTestBean bean = (ScopedTestBean) context.getBean("prototype"); assertThat(bean).isNotNull(); @@ -92,7 +92,7 @@ void testPrototype() { } @Test - void testSingletonScopeWithNoProxy() { + void singletonScopeWithNoProxy() { RequestContextHolder.setRequestAttributes(oldRequestAttributes); ApplicationContext context = createContext(ScopedProxyMode.NO); ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); @@ -115,7 +115,7 @@ void testSingletonScopeWithNoProxy() { } @Test - void testSingletonScopeIgnoresProxyInterfaces() { + void singletonScopeIgnoresProxyInterfaces() { RequestContextHolder.setRequestAttributes(oldRequestAttributes); ApplicationContext context = createContext(ScopedProxyMode.INTERFACES); ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); @@ -136,7 +136,7 @@ void testSingletonScopeIgnoresProxyInterfaces() { } @Test - void testSingletonScopeIgnoresProxyTargetClass() { + void singletonScopeIgnoresProxyTargetClass() { RequestContextHolder.setRequestAttributes(oldRequestAttributes); ApplicationContext context = createContext(ScopedProxyMode.TARGET_CLASS); ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); @@ -157,7 +157,7 @@ void testSingletonScopeIgnoresProxyTargetClass() { } @Test - void testRequestScopeWithNoProxy() { + void requestScopeWithNoProxy() { RequestContextHolder.setRequestAttributes(oldRequestAttributes); ApplicationContext context = createContext(ScopedProxyMode.NO); ScopedTestBean bean = (ScopedTestBean) context.getBean("request"); @@ -178,15 +178,14 @@ void testRequestScopeWithNoProxy() { } @Test - void testRequestScopeWithProxiedInterfaces() { + void requestScopeWithProxiedInterfaces() { RequestContextHolder.setRequestAttributes(oldRequestAttributes); ApplicationContext context = createContext(ScopedProxyMode.INTERFACES); IScopedTestBean bean = (IScopedTestBean) context.getBean("request"); // should be dynamic proxy, implementing both interfaces assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); - boolean condition = bean instanceof AnotherScopeTestInterface; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(AnotherScopeTestInterface.class); assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); bean.setName(MODIFIED_NAME); @@ -200,15 +199,14 @@ void testRequestScopeWithProxiedInterfaces() { } @Test - void testRequestScopeWithProxiedTargetClass() { + void requestScopeWithProxiedTargetClass() { RequestContextHolder.setRequestAttributes(oldRequestAttributes); ApplicationContext context = createContext(ScopedProxyMode.TARGET_CLASS); IScopedTestBean bean = (IScopedTestBean) context.getBean("request"); // should be a class-based proxy assertThat(AopUtils.isCglibProxy(bean)).isTrue(); - boolean condition = bean instanceof RequestScopedTestBean; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(RequestScopedTestBean.class); assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); bean.setName(MODIFIED_NAME); @@ -222,7 +220,7 @@ void testRequestScopeWithProxiedTargetClass() { } @Test - void testSessionScopeWithNoProxy() { + void sessionScopeWithNoProxy() { RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); ApplicationContext context = createContext(ScopedProxyMode.NO); ScopedTestBean bean = (ScopedTestBean) context.getBean("session"); @@ -243,15 +241,14 @@ void testSessionScopeWithNoProxy() { } @Test - void testSessionScopeWithProxiedInterfaces() { + void sessionScopeWithProxiedInterfaces() { RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); ApplicationContext context = createContext(ScopedProxyMode.INTERFACES); IScopedTestBean bean = (IScopedTestBean) context.getBean("session"); // should be dynamic proxy, implementing both interfaces assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); - boolean condition = bean instanceof AnotherScopeTestInterface; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(AnotherScopeTestInterface.class); assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); bean.setName(MODIFIED_NAME); @@ -271,17 +268,15 @@ void testSessionScopeWithProxiedInterfaces() { } @Test - void testSessionScopeWithProxiedTargetClass() { + void sessionScopeWithProxiedTargetClass() { RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); ApplicationContext context = createContext(ScopedProxyMode.TARGET_CLASS); IScopedTestBean bean = (IScopedTestBean) context.getBean("session"); // should be a class-based proxy assertThat(AopUtils.isCglibProxy(bean)).isTrue(); - boolean condition1 = bean instanceof ScopedTestBean; - assertThat(condition1).isTrue(); - boolean condition = bean instanceof SessionScopedTestBean; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(ScopedTestBean.class); + assertThat(bean).isInstanceOf(SessionScopedTestBean.class); assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); bean.setName(MODIFIED_NAME); diff --git a/integration-tests/src/test/java/org/springframework/context/annotation/scope/ClassPathBeanDefinitionScannerScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/context/annotation/scope/ClassPathBeanDefinitionScannerScopeIntegrationTests.java index 504da54de572..7fb3c19670ac 100644 --- a/integration-tests/src/test/java/org/springframework/context/annotation/scope/ClassPathBeanDefinitionScannerScopeIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/context/annotation/scope/ClassPathBeanDefinitionScannerScopeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -166,8 +166,7 @@ void requestScopeWithProxiedInterfaces() { // should be dynamic proxy, implementing both interfaces assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); - boolean condition = bean instanceof AnotherScopeTestInterface; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(AnotherScopeTestInterface.class); assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); bean.setName(MODIFIED_NAME); @@ -188,8 +187,7 @@ void requestScopeWithProxiedTargetClass() { // should be a class-based proxy assertThat(AopUtils.isCglibProxy(bean)).isTrue(); - boolean condition = bean instanceof RequestScopedTestBean; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(RequestScopedTestBean.class); assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); bean.setName(MODIFIED_NAME); @@ -231,8 +229,7 @@ void sessionScopeWithProxiedInterfaces() { // should be dynamic proxy, implementing both interfaces assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); - boolean condition = bean instanceof AnotherScopeTestInterface; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(AnotherScopeTestInterface.class); assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); bean.setName(MODIFIED_NAME); @@ -259,10 +256,8 @@ void sessionScopeWithProxiedTargetClass() { // should be a class-based proxy assertThat(AopUtils.isCglibProxy(bean)).isTrue(); - boolean condition1 = bean instanceof ScopedTestBean; - assertThat(condition1).isTrue(); - boolean condition = bean instanceof SessionScopedTestBean; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(ScopedTestBean.class); + assertThat(bean).isInstanceOf(SessionScopedTestBean.class); assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); bean.setName(MODIFIED_NAME); diff --git a/integration-tests/src/test/java/org/springframework/core/env/Constants.java b/integration-tests/src/test/java/org/springframework/core/env/Constants.java new file mode 100644 index 000000000000..3e01775038ce --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/core/env/Constants.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.env; + +/** + * Constants used both locally and in scan* sub-packages + */ +public class Constants { + + public static final String XML_PATH = "org/springframework/core/env/EnvironmentSystemIntegrationTests-context.xml"; + + public static final String ENVIRONMENT_AWARE_BEAN_NAME = "envAwareBean"; + + public static final String PROD_BEAN_NAME = "prodBean"; + public static final String DEV_BEAN_NAME = "devBean"; + public static final String DERIVED_DEV_BEAN_NAME = "derivedDevBean"; + public static final String TRANSITIVE_BEAN_NAME = "transitiveBean"; + + public static final String PROD_ENV_NAME = "prod"; + public static final String DEV_ENV_NAME = "dev"; + public static final String DERIVED_DEV_ENV_NAME = "derivedDev"; +} 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 824099b4c2c6..05da3efd058f 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,15 +58,15 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; import static org.springframework.context.ConfigurableApplicationContext.ENVIRONMENT_BEAN_NAME; -import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DERIVED_DEV_BEAN_NAME; -import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DERIVED_DEV_ENV_NAME; -import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_BEAN_NAME; -import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_ENV_NAME; -import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.ENVIRONMENT_AWARE_BEAN_NAME; -import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_BEAN_NAME; -import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_ENV_NAME; -import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.TRANSITIVE_BEAN_NAME; -import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.XML_PATH; +import static org.springframework.core.env.Constants.DERIVED_DEV_BEAN_NAME; +import static org.springframework.core.env.Constants.DERIVED_DEV_ENV_NAME; +import static org.springframework.core.env.Constants.DEV_BEAN_NAME; +import static org.springframework.core.env.Constants.DEV_ENV_NAME; +import static org.springframework.core.env.Constants.ENVIRONMENT_AWARE_BEAN_NAME; +import static org.springframework.core.env.Constants.PROD_BEAN_NAME; +import static org.springframework.core.env.Constants.PROD_ENV_NAME; +import static org.springframework.core.env.Constants.TRANSITIVE_BEAN_NAME; +import static org.springframework.core.env.Constants.XML_PATH; /** * System integration tests for container support of the {@link Environment} API. @@ -87,7 +87,7 @@ * @author Sam Brannen * @see org.springframework.context.support.EnvironmentIntegrationTests */ -public class EnvironmentSystemIntegrationTests { +class EnvironmentSystemIntegrationTests { private final ConfigurableEnvironment prodEnv = new StandardEnvironment(); @@ -648,7 +648,7 @@ public Object transitiveBean() { } } - @Profile(DERIVED_DEV_ENV_NAME) + @Profile(Constants.DERIVED_DEV_ENV_NAME) @Configuration static class DerivedDevConfig extends DevConfig { @Bean @@ -666,24 +666,4 @@ public Object expressionBean() { } } - - /** - * Constants used both locally and in scan* sub-packages - */ - public static class Constants { - - public static final String XML_PATH = "org/springframework/core/env/EnvironmentSystemIntegrationTests-context.xml"; - - public static final String ENVIRONMENT_AWARE_BEAN_NAME = "envAwareBean"; - - public static final String PROD_BEAN_NAME = "prodBean"; - public static final String DEV_BEAN_NAME = "devBean"; - public static final String DERIVED_DEV_BEAN_NAME = "derivedDevBean"; - public static final String TRANSITIVE_BEAN_NAME = "transitiveBean"; - - public static final String PROD_ENV_NAME = "prod"; - public static final String DEV_ENV_NAME = "dev"; - public static final String DERIVED_DEV_ENV_NAME = "derivedDev"; - } - } diff --git a/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java b/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java index 248000ce7bc7..f4fa4bd49a37 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ class PropertyPlaceholderConfigurerEnvironmentIntegrationTests { @Test - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) void test() { GenericApplicationContext ctx = new GenericApplicationContext(); ctx.registerBeanDefinition("ppc", diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan1/Config.java b/integration-tests/src/test/java/org/springframework/core/env/scan1/Config.java index 8e86c302c7d4..f9d407ca1c1e 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/scan1/Config.java +++ b/integration-tests/src/test/java/org/springframework/core/env/scan1/Config.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan1/DevConfig.java b/integration-tests/src/test/java/org/springframework/core/env/scan1/DevConfig.java index d63e79752e9d..5347936c8124 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/scan1/DevConfig.java +++ b/integration-tests/src/test/java/org/springframework/core/env/scan1/DevConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; -@Profile(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_ENV_NAME) +@Profile(org.springframework.core.env.Constants.DEV_ENV_NAME) @Configuration class DevConfig { diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan1/ProdConfig.java b/integration-tests/src/test/java/org/springframework/core/env/scan1/ProdConfig.java index eaf7c9a551a4..eed6d482a357 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/scan1/ProdConfig.java +++ b/integration-tests/src/test/java/org/springframework/core/env/scan1/ProdConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; -@Profile(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_ENV_NAME) +@Profile(org.springframework.core.env.Constants.PROD_ENV_NAME) @Configuration class ProdConfig { diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan2/DevBean.java b/integration-tests/src/test/java/org/springframework/core/env/scan2/DevBean.java index 8142b06b8f62..e9051f4723d6 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/scan2/DevBean.java +++ b/integration-tests/src/test/java/org/springframework/core/env/scan2/DevBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -@Profile(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_ENV_NAME) -@Component(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_BEAN_NAME) +@Profile(org.springframework.core.env.Constants.DEV_ENV_NAME) +@Component(org.springframework.core.env.Constants.DEV_BEAN_NAME) class DevBean { } diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan2/ProdBean.java b/integration-tests/src/test/java/org/springframework/core/env/scan2/ProdBean.java index 75ee9d53c1b9..c5d5fb191dbf 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/scan2/ProdBean.java +++ b/integration-tests/src/test/java/org/springframework/core/env/scan2/ProdBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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.context.annotation.Profile; import org.springframework.stereotype.Component; -@Profile(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_ENV_NAME) -@Component(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_BEAN_NAME) +@Profile(org.springframework.core.env.Constants.PROD_ENV_NAME) +@Component(org.springframework.core.env.Constants.PROD_BEAN_NAME) class ProdBean { } 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 59da59d7ef58..42f9939a89f9 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/expression/spel/support/Spr7538Tests.java b/integration-tests/src/test/java/org/springframework/expression/spel/support/Spr7538Tests.java index 6fe548b5de8a..c115843665dc 100644 --- a/integration-tests/src/test/java/org/springframework/expression/spel/support/Spr7538Tests.java +++ b/integration-tests/src/test/java/org/springframework/expression/spel/support/Spr7538Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java index 050e3793f7f2..4a8f36e598f9 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java index 9b7eef5a0961..82c3b5a27203 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java index 510fc08d92f4..7fd3d61b922f 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/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 cf535ad9f4e6..ee20417a9a00 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,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,21 +17,35 @@ 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 /** @@ -41,83 +55,156 @@ 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 + suspend fun `Multiple interceptors with suspending function`() { + assertThat(firstAdvisor.interceptor.invocations).isEmpty() + assertThat(secondAdvisor.interceptor.invocations).isEmpty() + val value = "Hello!" + 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) } - } + } + + @Test // gh-33095 + suspend fun `Aspect and reactive transactional with suspending function`() { + assertThat(countingAspect.counter).isZero() + assertThat(reactiveTransactionManager.commits).isZero() + val value = "Hello!" + 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 + suspend fun `Aspect and cacheable with suspending function`() { + assertThat(countingAspect.counter).isZero() + val value = "Hello!" + 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++}" + } - @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 - } - - } + } } diff --git a/settings.gradle b/settings.gradle index 3bc6898a5ba3..052b11485e89 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,5 @@ plugins { - id "com.gradle.develocity" version "3.17.2" - id "io.spring.ge.conventions" version "0.0.17" - id "org.gradle.toolchains.foojay-resolver-convention" version "0.7.0" + id "io.spring.develocity.conventions" version "0.0.25" } include "spring-aop" @@ -14,7 +12,6 @@ include "spring-core" include "spring-core-test" include "spring-expression" include "spring-instrument" -include "spring-jcl" include "spring-jdbc" include "spring-jms" include "spring-messaging" diff --git a/spring-aop/spring-aop.gradle b/spring-aop/spring-aop.gradle index 2e166980450d..eec30b7bedff 100644 --- a/spring-aop/spring-aop.gradle +++ b/spring-aop/spring-aop.gradle @@ -5,12 +5,12 @@ apply plugin: "kotlin" dependencies { api(project(":spring-beans")) api(project(":spring-core")) + compileOnly("com.google.code.findbugs:jsr305") // for the AOP Alliance fork optional("org.apache.commons:commons-pool2") optional("org.aspectj:aspectjweaver") optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") testFixturesImplementation(testFixtures(project(":spring-beans"))) testFixturesImplementation(testFixtures(project(":spring-core"))) - testFixturesImplementation("com.google.code.findbugs:jsr305") testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-core"))) diff --git a/spring-aop/src/main/java/org/aopalliance/aop/Advice.java b/spring-aop/src/main/java/org/aopalliance/aop/Advice.java index 38f999945810..d43c9564f8b1 100644 --- a/spring-aop/src/main/java/org/aopalliance/aop/Advice.java +++ b/spring-aop/src/main/java/org/aopalliance/aop/Advice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/aopalliance/aop/AspectException.java b/spring-aop/src/main/java/org/aopalliance/aop/AspectException.java index a91c2ac61edc..fbff862bf474 100644 --- a/spring-aop/src/main/java/org/aopalliance/aop/AspectException.java +++ b/spring-aop/src/main/java/org/aopalliance/aop/AspectException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..f4db0d42ebe7 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInterceptor.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,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..807d04a682de 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInvocation.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInvocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ 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/Interceptor.java b/spring-aop/src/main/java/org/aopalliance/intercept/Interceptor.java index 918e080ff448..8cd233881f0a 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/Interceptor.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/Interceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java b/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java index 96caaefefe00..362d4f533433 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author 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.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..3db338435f61 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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,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..5e3c66f56e32 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/MethodInterceptor.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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..6f5933c714b9 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/MethodInvocation.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInvocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ 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/Advisor.java b/spring-aop/src/main/java/org/springframework/aop/Advisor.java index 100fbd07797e..0cfa3edd54bb 100644 --- a/spring-aop/src/main/java/org/springframework/aop/Advisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/Advisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/AfterAdvice.java b/spring-aop/src/main/java/org/springframework/aop/AfterAdvice.java index 641a7018424a..b8b6208435c1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/AfterAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/AfterAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/AfterReturningAdvice.java b/spring-aop/src/main/java/org/springframework/aop/AfterReturningAdvice.java index 8c2c5d6ef8f0..348649ddafb7 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-present the original author 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/AopInvocationException.java b/spring-aop/src/main/java/org/springframework/aop/AopInvocationException.java index 1acee1559c21..a13fdcc615a6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/AopInvocationException.java +++ b/spring-aop/src/main/java/org/springframework/aop/AopInvocationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/BeforeAdvice.java b/spring-aop/src/main/java/org/springframework/aop/BeforeAdvice.java index 80123654468f..b28a99afbe71 100644 --- a/spring-aop/src/main/java/org/springframework/aop/BeforeAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/BeforeAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/ClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/ClassFilter.java index 7926be708e33..56a5e7fe2016 100644 --- a/spring-aop/src/main/java/org/springframework/aop/ClassFilter.java +++ b/spring-aop/src/main/java/org/springframework/aop/ClassFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java b/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java index 2f46775b9459..1fb07292c78e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/IntroductionAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/IntroductionAdvisor.java index cd5f52c37e36..36ea5a084448 100644 --- a/spring-aop/src/main/java/org/springframework/aop/IntroductionAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/IntroductionAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java index 181237b2a8fc..ff02cb8b8b47 100644 --- a/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/IntroductionInfo.java b/spring-aop/src/main/java/org/springframework/aop/IntroductionInfo.java index 534e2dbc9841..a24c91f3d4a3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/IntroductionInfo.java +++ b/spring-aop/src/main/java/org/springframework/aop/IntroductionInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/IntroductionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/IntroductionInterceptor.java index 5a8ba212d7e3..9eae9776131c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/IntroductionInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/IntroductionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/MethodBeforeAdvice.java b/spring-aop/src/main/java/org/springframework/aop/MethodBeforeAdvice.java index 806744d09c31..1fd3ca6f30e3 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-present the original author 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..997c8714c090 100644 --- a/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..00a558411d7e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/Pointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/Pointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ * *

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/PointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/PointcutAdvisor.java index 69eb504eb032..4d445f25c88a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/PointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/PointcutAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java b/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java index 2cc637621c90..ea9838dd2479 100644 --- a/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java +++ b/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author 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,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 + * Set the arguments to be used on subsequent invocations in 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/RawTargetAccess.java b/spring-aop/src/main/java/org/springframework/aop/RawTargetAccess.java index 7a15b3495ce4..a2fa8293344b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/RawTargetAccess.java +++ b/spring-aop/src/main/java/org/springframework/aop/RawTargetAccess.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java b/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java index 95057e0d7dba..02a26fc94efb 100644 --- a/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java b/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java index d518ddb444a0..c36adc2713c9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java +++ b/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author 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; /** * Minimal interface for exposing the target class behind a proxy. @@ -36,7 +36,6 @@ public interface TargetClassAware { * (typically a proxy configuration or an actual proxy). * @return the target Class, or {@code null} if not known */ - @Nullable - Class getTargetClass(); + @Nullable Class getTargetClass(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/TargetSource.java b/spring-aop/src/main/java/org/springframework/aop/TargetSource.java index c19982f31916..fbb4f0c484d2 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-2023 the original author or authors. + * Copyright 2002-present the original author 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,8 +42,7 @@ public interface TargetSource extends TargetClassAware { * @return the type of targets returned by this {@link TargetSource} */ @Override - @Nullable - Class getTargetClass(); + @Nullable Class getTargetClass(); /** * Will all calls to {@link #getTarget()} return the same object? @@ -64,8 +63,7 @@ default boolean isStatic() { * or {@code null} if there is no actual target instance * @throws Exception if the target object can't be resolved */ - @Nullable - Object getTarget() throws Exception; + @Nullable Object getTarget() throws Exception; /** * Release the given target object obtained from the diff --git a/spring-aop/src/main/java/org/springframework/aop/ThrowsAdvice.java b/spring-aop/src/main/java/org/springframework/aop/ThrowsAdvice.java index ef50fe8b8263..c4e43ba0a9f1 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-present the original author 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/TrueClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/TrueClassFilter.java index b5bd71f6f1b2..cf081cb6e3ba 100644 --- a/spring-aop/src/main/java/org/springframework/aop/TrueClassFilter.java +++ b/spring-aop/src/main/java/org/springframework/aop/TrueClassFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java index 6498627d6fe2..9a45f3fd983e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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/TruePointcut.java b/spring-aop/src/main/java/org/springframework/aop/TruePointcut.java index f767d75d2f15..76b255f53925 100644 --- a/spring-aop/src/main/java/org/springframework/aop/TruePointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/TruePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index 2e2ee857f3d4..bb75e0f23bbc 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-present the original author 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,14 +553,13 @@ private void configurePointcutParameters(String[] argumentNames, int argumentInd * @param ex the exception thrown by the method execution (may be null) * @return the empty array if there are no arguments */ - @SuppressWarnings("NullAway") - protected Object[] argBinding(JoinPoint jp, @Nullable JoinPointMatch jpMatch, + protected @Nullable Object[] argBinding(JoinPoint jp, @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex) { calculateArgumentBindings(); // AMC start - Object[] adviceInvocationArgs = new Object[this.parameterTypes.length]; + @Nullable Object[] adviceInvocationArgs = new Object[this.parameterTypes.length]; int numBound = 0; if (this.joinPointArgumentIndex != -1) { @@ -578,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++; } @@ -585,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++; } @@ -614,28 +617,35 @@ else if (this.joinPointStaticPartArgumentIndex != -1) { * @return the invocation result * @throws Throwable in case of invocation failure */ - protected Object invokeAdviceMethod( - @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex) - throws Throwable { + protected @Nullable Object invokeAdviceMethod(@Nullable JoinPointMatch jpMatch, + @Nullable Object returnValue, @Nullable Throwable ex) throws Throwable { return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex)); } // As above, but in this case we are given the join point. - protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch, + protected @Nullable Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable t) throws Throwable { return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t)); } - protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable { - Object[] actualArgs = args; + protected @Nullable Object invokeAdviceMethodWithGivenArgs(@Nullable Object[] args) throws Throwable { + @Nullable Object[] actualArgs = args; if (this.aspectJAdviceMethod.getParameterCount() == 0) { actualArgs = null; } + Object aspectInstance = this.aspectInstanceFactory.getAspectInstance(); + if (aspectInstance.equals(null)) { + // Possibly a NullBean -> simply proceed if necessary. + if (getJoinPoint() instanceof ProceedingJoinPoint pjp) { + return pjp.proceed(); + } + return null; + } try { ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); - return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); + return this.aspectJAdviceMethod.invoke(aspectInstance, actualArgs); } catch (IllegalArgumentException ex) { throw new AopInvocationException("Mismatch on arguments to advice method [" + @@ -657,8 +667,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); @@ -672,8 +681,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..a402e6cd487d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..feeb53f0b210 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-present the original author 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..70a9aeb5ee09 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,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..b3eeb95e284c 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-present the original author 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..42a5467d9fc5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,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(); } @@ -76,7 +75,7 @@ public Object invoke(MethodInvocation mi) throws Throwable { * is only invoked if the thrown exception is a subtype of the given throwing type. */ private boolean shouldInvokeOnThrowing(Throwable ex) { - return getDiscoveredThrowingType().isAssignableFrom(ex.getClass()); + return getDiscoveredThrowingType().isInstance(ex); } } 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..a976858a29d6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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,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..05529311b005 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,9 @@ 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 1a08bb454d87..6452670bf644 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.aop.aspectj; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; @@ -38,6 +39,7 @@ import org.aspectj.weaver.tools.PointcutPrimitive; import org.aspectj.weaver.tools.ShadowMatch; import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; +import org.jspecify.annotations.Nullable; import org.springframework.aop.ClassFilter; import org.springframework.aop.IntroductionAwareMethodMatcher; @@ -53,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; @@ -82,6 +83,8 @@ 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, @@ -96,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 volatile @Nullable ClassLoader pointcutClassLoader; - @Nullable - private transient PointcutExpression pointcutExpression; + private transient volatile @Nullable PointcutExpression pointcutExpression; - private transient boolean pointcutParsingFailed = false; + private transient volatile boolean pointcutParsingFailed; /** @@ -128,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"); @@ -143,6 +144,7 @@ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, */ public void setPointcutDeclarationScope(Class pointcutDeclarationScope) { this.pointcutDeclarationScope = pointcutDeclarationScope; + this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope); } /** @@ -191,18 +193,20 @@ private void checkExpression() { * Lazily build the underlying AspectJ pointcut expression. */ private PointcutExpression obtainPointcutExpression() { - if (this.pointcutExpression == null) { - this.pointcutClassLoader = determinePointcutClassLoader(); - this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); + PointcutExpression pointcutExpression = this.pointcutExpression; + if (pointcutExpression == null) { + ClassLoader pointcutClassLoader = determinePointcutClassLoader(); + pointcutExpression = buildPointcutExpression(pointcutClassLoader); + this.pointcutClassLoader = pointcutClassLoader; + this.pointcutExpression = pointcutExpression; } - return this.pointcutExpression; + return pointcutExpression; } /** * Determine the ClassLoader to use for pointcut evaluation. */ - @Nullable - private ClassLoader determinePointcutClassLoader() { + private @Nullable ClassLoader determinePointcutClassLoader() { if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { return cbf.getBeanClassLoader(); } @@ -268,6 +272,11 @@ public PointcutExpression getPointcutExpression() { @Override public boolean matches(Class targetClass) { 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; } @@ -334,7 +343,7 @@ public boolean isRuntime() { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { + public boolean matches(Method method, Class targetClass, @Nullable Object... args) { ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target, @@ -392,8 +401,7 @@ public boolean matches(Method method, Class targetClass, Object... args) { } } - @Nullable - protected String getCurrentProxiedBeanName() { + protected @Nullable String getCurrentProxiedBeanName() { return ProxyCreationContext.getCurrentProxiedBeanName(); } @@ -401,8 +409,7 @@ protected String getCurrentProxiedBeanName() { /** * Get a new pointcut expression based on a target class's loader rather than the default. */ - @Nullable - private PointcutExpression getFallbackPointcutExpression(Class targetClass) { + private @Nullable PointcutExpression getFallbackPointcutExpression(Class targetClass) { try { ClassLoader classLoader = targetClass.getClassLoader(); if (classLoader != null && classLoader != this.pointcutClassLoader) { @@ -456,40 +463,24 @@ private ShadowMatch getTargetShadowMatch(Method method, Class targetClass) { } private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { - ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(this, targetMethod); + ShadowMatchKey key = new ShadowMatchKey(this, targetMethod); + ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(key); if (shadowMatch == null) { - PointcutExpression fallbackExpression = null; - Method methodToMatch = targetMethod; - try { - try { - shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); - } - catch (ReflectionWorldException ex) { - // Failed to introspect target method, probably because it has been loaded - // in a special ClassLoader. Let's try the declaring ClassLoader instead... - try { - fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); - if (fallbackExpression != null) { - shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); - } - } - catch (ReflectionWorldException ex2) { - fallbackExpression = null; - } + PointcutExpression pointcutExpression = obtainPointcutExpression(); + synchronized (pointcutExpression) { + shadowMatch = ShadowMatchUtils.getShadowMatch(key); + if (shadowMatch != null) { + return shadowMatch; } - if (targetMethod != originalMethod && (shadowMatch == null || - (Proxy.isProxyClass(targetMethod.getDeclaringClass()) && - (shadowMatch.neverMatches() || containsAnnotationPointcut())))) { - // Fall back to the plain original method in case of no resolvable match or a - // negative match on a proxy class (which doesn't carry any annotations on its - // redeclared methods), as well as for annotation pointcuts. - methodToMatch = originalMethod; + PointcutExpression fallbackExpression = null; + Method methodToMatch = targetMethod; + try { try { - shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); + shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch); } catch (ReflectionWorldException ex) { - // Could neither introspect the target class nor the proxy class -> - // let's try the original method's declaring class before we give up... + // Failed to introspect target method, probably because it has been loaded + // in a special ClassLoader. Let's try the declaring ClassLoader instead... try { fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); if (fallbackExpression != null) { @@ -500,21 +491,45 @@ private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { fallbackExpression = null; } } + if (targetMethod != originalMethod && (shadowMatch == null || + (Proxy.isProxyClass(targetMethod.getDeclaringClass()) && + (shadowMatch.neverMatches() || containsAnnotationPointcut())))) { + // Fall back to the plain original method in case of no resolvable match or a + // negative match on a proxy class (which doesn't carry any annotations on its + // redeclared methods), as well as for annotation pointcuts. + methodToMatch = originalMethod; + try { + shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch); + } + catch (ReflectionWorldException ex) { + // Could neither introspect the target class nor the proxy class -> + // let's try the original method's declaring class before we give up... + try { + fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); + if (fallbackExpression != null) { + shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); + } + } + catch (ReflectionWorldException ex2) { + fallbackExpression = null; + } + } + } } + catch (Throwable ex) { + // Possibly AspectJ 1.8.10 encountering an invalid signature + logger.debug("PointcutExpression matching rejected target method", ex); + fallbackExpression = null; + } + if (shadowMatch == null) { + shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null); + } + else if (shadowMatch.maybeMatches() && fallbackExpression != null) { + shadowMatch = new DefensiveShadowMatch(shadowMatch, + fallbackExpression.matchesMethodExecution(methodToMatch)); + } + shadowMatch = ShadowMatchUtils.setShadowMatch(key, shadowMatch); } - catch (Throwable ex) { - // Possibly AspectJ 1.8.10 encountering an invalid signature - logger.debug("PointcutExpression matching rejected target method", ex); - fallbackExpression = null; - } - if (shadowMatch == null) { - shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null); - } - else if (shadowMatch.maybeMatches() && fallbackExpression != null) { - shadowMatch = new DefensiveShadowMatch(shadowMatch, - fallbackExpression.matchesMethodExecution(methodToMatch)); - } - shadowMatch = ShadowMatchUtils.setShadowMatch(this, targetMethod, shadowMatch); } return shadowMatch; } @@ -523,6 +538,16 @@ 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) { @@ -602,14 +627,14 @@ public BeanContextMatcher(String expression) { @Override @SuppressWarnings("rawtypes") - @Deprecated + @Deprecated(since = "4.0") // deprecated by AspectJ public boolean couldMatchJoinPointsInType(Class someClass) { return (contextMatch(someClass) == FuzzyBoolean.YES); } @Override @SuppressWarnings("rawtypes") - @Deprecated + @Deprecated(since = "4.0") // deprecated by AspectJ public boolean couldMatchJoinPointsInType(Class someClass, MatchingContext context) { return (contextMatch(someClass) == FuzzyBoolean.YES); } @@ -699,4 +724,8 @@ public void setMatchingContext(MatchingContext aMatchContext) { } } + + private record ShadowMatchKey(AspectJExpressionPointcut expression, Method method) { + } + } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisor.java index 9f4b1e990d8e..931cbcfac33f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..679adb5c20fa 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-present the original author 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..a9a128248729 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,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/AspectJPrecedenceInformation.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPrecedenceInformation.java index 88c946887670..9ebf34ace31d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPrecedenceInformation.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPrecedenceInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java index be7c8569404b..86dfe04b0f8e 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-present the original author 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/AspectJWeaverMessageHandler.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJWeaverMessageHandler.java index ed837d8c9415..c5c4d35f3c53 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJWeaverMessageHandler.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJWeaverMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/DeclareParentsAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/DeclareParentsAdvisor.java index 43233666adf3..8d515853e31c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/DeclareParentsAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/DeclareParentsAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/InstantiationModelAwarePointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/InstantiationModelAwarePointcutAdvisor.java index 2f3ddab33c30..bb5a78bc4b5d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/InstantiationModelAwarePointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/InstantiationModelAwarePointcutAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2006 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java index 68eb55c9c4a6..a430c3553352 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-present the original author 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,10 @@ 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; /** @@ -51,20 +50,15 @@ */ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint, JoinPoint.StaticPart { - private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); - 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 +78,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 +106,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(); } @@ -171,7 +163,7 @@ public String toLongString() { @Override public String toString() { - return "execution(" + getSignature().toString() + ")"; + return "execution(" + getSignature() + ")"; } @@ -180,8 +172,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,11 +210,11 @@ 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()); + parameterNames = DefaultParameterNameDiscoverer.getSharedInstance().getParameterNames(getMethod()); this.parameterNames = parameterNames; } return parameterNames; @@ -325,7 +316,7 @@ public int getLine() { } @Override - @Deprecated + @Deprecated(since = "4.0") // deprecated by AspectJ public int getColumn() { throw new UnsupportedOperationException(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/RuntimeTestWalker.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/RuntimeTestWalker.java index bf37296a6e8a..9833192b9f32 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,8 @@ import org.aspectj.weaver.reflect.ReflectionVar; import org.aspectj.weaver.reflect.ShadowMatchImpl; import org.aspectj.weaver.tools.ShadowMatch; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -79,8 +79,7 @@ class RuntimeTestWalker { } - @Nullable - private final Test runtimeTest; + private final @Nullable Test runtimeTest; public RuntimeTestWalker(ShadowMatch shadowMatch) { diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java index beb3ac63bb96..cd1007848973 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/ShadowMatchUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,60 +16,52 @@ package org.springframework.aop.aspectj; -import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.aspectj.weaver.tools.ShadowMatch; - -import org.springframework.aop.support.ExpressionPointcut; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Internal {@link ShadowMatch} utilities. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 6.2 */ public abstract class ShadowMatchUtils { - private static final Map shadowMatchCache = new ConcurrentHashMap<>(256); + private static final Map shadowMatchCache = new ConcurrentHashMap<>(256); - /** - * Clear the cache of computed {@link ShadowMatch} instances. - */ - public static void clearCache() { - shadowMatchCache.clear(); - } /** - * Return the {@link ShadowMatch} for the specified {@link ExpressionPointcut} - * and {@link Method} or {@code null} if none is found. - * @param expression the expression - * @param method the method - * @return the {@code ShadowMatch} to use for the specified expression and method + * Find a {@link ShadowMatch} for the specified key. + * @param key the key to use + * @return the {@code ShadowMatch} to use for the specified key, + * or {@code null} if none found */ - @Nullable - static ShadowMatch getShadowMatch(ExpressionPointcut expression, Method method) { - return shadowMatchCache.get(new Key(expression, method)); + static @Nullable ShadowMatch getShadowMatch(Object key) { + return shadowMatchCache.get(key); } /** - * Associate the {@link ShadowMatch} to the specified {@link ExpressionPointcut} - * and method. If an entry already exists, the given {@code shadowMatch} is - * ignored. - * @param expression the expression - * @param method the method - * @param shadowMatch the shadow match to use for this expression and method + * Associate the {@link ShadowMatch} with the specified key. + * If an entry already exists, the given {@code shadowMatch} is ignored. + * @param key the key to use + * @param shadowMatch the shadow match to use for this key * if none already exists - * @return the shadow match to use for the specified expression and method + * @return the shadow match to use for the specified key */ - static ShadowMatch setShadowMatch(ExpressionPointcut expression, Method method, ShadowMatch shadowMatch) { - ShadowMatch existing = shadowMatchCache.putIfAbsent(new Key(expression, method), shadowMatch); + static ShadowMatch setShadowMatch(Object key, ShadowMatch shadowMatch) { + ShadowMatch existing = shadowMatchCache.putIfAbsent(key, shadowMatch); return (existing != null ? existing : shadowMatch); } - - private record Key(ExpressionPointcut expression, Method method) {} + /** + * Clear the cache of computed {@link ShadowMatch} instances. + */ + public static void clearCache() { + shadowMatchCache.clear(); + } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/SimpleAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/SimpleAspectInstanceFactory.java index f8a674ab13e6..86e43610cb29 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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,13 @@ package org.springframework.aop.aspectj; +import java.lang.reflect.InaccessibleObjectException; 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; @@ -66,7 +68,7 @@ public final Object getAspectInstance() { throw new AopConfigException( "Unable to instantiate aspect class: " + this.aspectClass.getName(), ex); } - catch (IllegalAccessException ex) { + catch (IllegalAccessException | InaccessibleObjectException ex) { throw new AopConfigException( "Could not access aspect constructor: " + this.aspectClass.getName(), ex); } @@ -77,8 +79,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..5722d990682b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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..441074d8604b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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 6f0eef820701..89d61d23d93e 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.annotation.Annotation; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map; import java.util.StringTokenizer; @@ -34,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 @@ -58,6 +60,23 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac 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()); @@ -67,7 +86,8 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac @Override public boolean isAspect(Class clazz) { - return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null); + return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null && + (!shouldIgnoreAjcCompiledAspects || !compiledByAjc(clazz))); } @Override @@ -92,8 +112,7 @@ public void validate(Class aspectClass) throws AopConfigException { * (there should only be one anyway...). */ @SuppressWarnings("unchecked") - @Nullable - protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) { + protected static @Nullable AspectJAnnotation findAspectJAnnotationOnMethod(Method method) { for (Class annotationType : ASPECTJ_ANNOTATION_CLASSES) { AspectJAnnotation annotation = findAnnotation(method, (Class) annotationType); if (annotation != null) { @@ -103,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); @@ -114,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. @@ -213,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; } @@ -237,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..203e4767e79a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,12 @@ import java.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 index 7149816f5742..bf6c07c8173f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.Field; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -38,14 +39,13 @@ class AspectJAdvisorBeanRegistrationAotProcessor implements BeanRegistrationAotP private static final String AJC_MAGIC = "ajc$"; - private static final boolean aspectjPresent = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", + private static final boolean ASPECTJ_PRESENT = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", AspectJAdvisorBeanRegistrationAotProcessor.class.getClassLoader()); @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { - if (aspectjPresent) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + if (ASPECTJ_PRESENT) { Class beanClass = registeredBean.getBeanClass(); if (compiledByAjc(beanClass)) { return new AspectJAdvisorContribution(beanClass); @@ -74,7 +74,7 @@ public AspectJAdvisorContribution(Class beanClass) { @Override public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { - generationContext.getRuntimeHints().reflection().registerType(this.beanClass, MemberCategory.DECLARED_FIELDS); + generationContext.getRuntimeHints().reflection().registerType(this.beanClass, MemberCategory.ACCESS_DECLARED_FIELDS); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java index c3bf1685297e..75483f30fd32 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,11 @@ import java.util.List; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.aspectj.AspectJExpressionPointcut; import org.springframework.aop.framework.AopConfigException; -import org.springframework.lang.Nullable; /** * Interface for factories that can create Spring AOP Advisors from classes @@ -80,8 +80,7 @@ public interface AspectJAdvisorFactory { * or if it is a pointcut that will be used by other advice but will not * create a Spring advice in its own right */ - @Nullable - Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, + @Nullable Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName); /** @@ -100,8 +99,7 @@ Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFact * @see org.springframework.aop.aspectj.AspectJAfterReturningAdvice * @see org.springframework.aop.aspectj.AspectJAfterThrowingAdvice */ - @Nullable - Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + @Nullable Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java index 71e7bea2b4ac..32ecda58883a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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,14 +41,13 @@ */ class AspectJBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { - private static final boolean aspectJPresent = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", + private static final boolean ASPECTJ_PRESENT = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", AspectJBeanFactoryInitializationAotProcessor.class.getClassLoader()); @Override - @Nullable - public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { - if (aspectJPresent) { + public @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + if (ASPECTJ_PRESENT) { return AspectDelegate.processAheadOfTime(beanFactory); } return null; @@ -59,8 +59,7 @@ public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableL */ private static class AspectDelegate { - @Nullable - private static AspectContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + private static @Nullable AspectContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { BeanFactoryAspectJAdvisorsBuilder builder = new BeanFactoryAspectJAdvisorsBuilder(beanFactory); List advisors = builder.buildAspectJAdvisors(); return (advisors.isEmpty() ? null : new AspectContribution(advisors)); diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java index c3437b5d3dc1..ffd6d00ad216 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,7 +83,7 @@ public AspectJProxyFactory(Class... interfaces) { /** * Add the supplied aspect instance to the chain. The type of the aspect instance - * supplied must be a singleton aspect. True singleton lifecycle is not honoured when + * supplied must be a singleton aspect. True singleton lifecycle is not honored when * using this method - the caller is responsible for managing the lifecycle of any * aspects added in this way. * @param aspectInstance the AspectJ aspect instance 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 a70aed625cc9..c404b786bb50 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -160,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 28d5aa13e50f..599ee6d72701 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,13 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.Ordered; import org.springframework.core.annotation.OrderUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -91,8 +93,7 @@ public Object getAspectInstance() { } @Override - @Nullable - public ClassLoader getAspectClassLoader() { + public @Nullable ClassLoader getAspectClassLoader() { return (this.beanFactory instanceof ConfigurableBeanFactory cbf ? cbf.getBeanClassLoader() : ClassUtils.getDefaultClassLoader()); } @@ -103,8 +104,7 @@ public AspectMetadata getAspectMetadata() { } @Override - @Nullable - public Object getAspectCreationMutex() { + public @Nullable Object getAspectCreationMutex() { if (this.beanFactory.isSingleton(this.name)) { // Rely on singleton semantics provided by the factory -> no local lock. return null; @@ -130,7 +130,12 @@ public int getOrder() { Class type = this.beanFactory.getType(this.name); if (type != null) { if (Ordered.class.isAssignableFrom(type) && this.beanFactory.isSingleton(this.name)) { - return ((Ordered) this.beanFactory.getBean(this.name)).getOrder(); + try { + return this.beanFactory.getBean(this.name, Ordered.class).getOrder(); + } + catch (BeanNotOfRequiredTypeException ex) { + // Not actually implementing Ordered -> possibly a NullBean. + } } return OrderUtils.getOrder(type, Ordered.LOWEST_PRECEDENCE); } 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 2ac439af1e8b..23b0ddc095e9 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-present the original author 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<>(); @@ -80,7 +84,6 @@ public BeanFactoryAspectJAdvisorsBuilder(ListableBeanFactory beanFactory, Aspect * @return the list of {@link org.springframework.aop.Advisor} beans * @see #isEligibleBean */ - @SuppressWarnings("NullAway") public List buildAspectJAdvisors() { List aspectNames = this.aspectBeanNames; @@ -103,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)); } } } @@ -147,6 +157,7 @@ public List buildAspectJAdvisors() { } else { MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); + Assert.state(factory != null, "Factory must not be null"); advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java index db20f7608131..fd5cca68a90a 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,14 +73,11 @@ final class InstantiationModelAwarePointcutAdvisorImpl private final boolean lazy; - @Nullable - private Advice instantiatedAdvice; + private @Nullable Advice instantiatedAdvice; - @Nullable - private Boolean isBeforeAdvice; + private @Nullable Boolean isBeforeAdvice; - @Nullable - private Boolean isAfterAdvice; + private @Nullable Boolean isAfterAdvice; public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut, @@ -120,7 +117,7 @@ public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut decl /** * The pointcut for Spring AOP to use. - * Actual behaviour of the pointcut will change depending on the state of the advice. + * Actual behavior of the pointcut will change depending on the state of the advice. */ @Override public Pointcut getPointcut() { @@ -195,21 +192,19 @@ public int getDeclarationOrder() { } @Override - @SuppressWarnings("NullAway") public boolean isBeforeAdvice() { if (this.isBeforeAdvice == null) { determineAdviceType(); } - return this.isBeforeAdvice; + return (this.isBeforeAdvice == Boolean.TRUE); } @Override - @SuppressWarnings("NullAway") public boolean isAfterAdvice() { if (this.isAfterAdvice == null) { determineAdviceType(); } - return this.isAfterAdvice; + return (this.isAfterAdvice == Boolean.TRUE); } /** @@ -261,7 +256,7 @@ public String toString() { /** - * Pointcut implementation that changes its behaviour when the advice is instantiated. + * Pointcut implementation that changes its behavior when the advice is instantiated. * Note that this is a dynamic pointcut; otherwise it might be optimized out * if it does not at first match statically. */ @@ -271,8 +266,7 @@ private static final class PerTargetInstantiationModelPointcut extends DynamicMe private final Pointcut preInstantiationPointcut; - @Nullable - private LazySingletonAspectInstanceFactoryDecorator aspectInstanceFactory; + private @Nullable LazySingletonAspectInstanceFactoryDecorator aspectInstanceFactory; public PerTargetInstantiationModelPointcut(AspectJExpressionPointcut declaredPointcut, Pointcut preInstantiationPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory) { @@ -293,7 +287,7 @@ public boolean matches(Method method, Class targetClass) { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { + public boolean matches(Method method, Class targetClass, @Nullable Object... args) { // This can match only on declared pointcut. return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass, args)); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/LazySingletonAspectInstanceFactoryDecorator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/LazySingletonAspectInstanceFactoryDecorator.java index 73ba36c79dc3..b730078f9934 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..2ac451a2e9c7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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/NotAnAtAspectException.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/NotAnAtAspectException.java index 7db2a4cb1eb4..626fc61aa1ef 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/NotAnAtAspectException.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/NotAnAtAspectException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/PrototypeAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/PrototypeAspectInstanceFactory.java index ee295523e2d1..ee27f9a2ad46 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/PrototypeAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/PrototypeAspectInstanceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java index e4eec7a919d9..95020ecd59ee 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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; @@ -47,9 +48,7 @@ import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.factory.BeanFactory; 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; @@ -84,10 +83,10 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto // @AfterThrowing methods due to the fact that AspectJAfterAdvice.invoke(MethodInvocation) // invokes proceed() in a `try` block and only invokes the @After advice method // in a corresponding `finally` block. - Comparator adviceKindComparator = new ConvertingComparator<>( + Comparator adviceKindComparator = new ConvertingComparator( new InstanceComparator<>( Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class), - (Converter) method -> { + method -> { AspectJAnnotation ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method); return (ann != null ? ann.getAnnotation() : null); }); @@ -96,8 +95,7 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto } - @Nullable - private final BeanFactory beanFactory; + private final @Nullable BeanFactory beanFactory; /** @@ -111,7 +109,7 @@ public ReflectiveAspectJAdvisorFactory() { * Create a new {@code ReflectiveAspectJAdvisorFactory}, propagating the given * {@link BeanFactory} to the created {@link AspectJExpressionPointcut} instances, * for bean pointcut handling as well as consistent {@link ClassLoader} resolution. - * @param beanFactory the BeanFactory to propagate (may be {@code null}} + * @param beanFactory the BeanFactory to propagate (may be {@code null}) * @since 4.3.6 * @see AspectJExpressionPointcut#setBeanFactory * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanClassLoader() @@ -183,8 +181,7 @@ private List getAdvisorMethods(Class aspectClass) { * @param introductionField the field to introspect * @return the Advisor instance, or {@code null} if not an Advisor */ - @Nullable - private Advisor getDeclareParentsAdvisor(Field introductionField) { + private @Nullable Advisor getDeclareParentsAdvisor(Field introductionField) { DeclareParents declareParents = introductionField.getAnnotation(DeclareParents.class); if (declareParents == null) { // Not an introduction field @@ -201,8 +198,7 @@ private Advisor getDeclareParentsAdvisor(Field introductionField) { @Override - @Nullable - public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, + public @Nullable Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrderInAspect, String aspectName) { validate(aspectInstanceFactory.getAspectMetadata().getAspectClass()); @@ -225,8 +221,7 @@ public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInsta } } - @Nullable - private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { + private @Nullable AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { AspectJAnnotation aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); if (aspectJAnnotation == null) { @@ -244,8 +239,7 @@ private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Clas @Override - @Nullable - public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + public @Nullable Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { Class candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); @@ -307,7 +301,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/SimpleMetadataAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SimpleMetadataAwareAspectInstanceFactory.java index 386d791130ec..c1734cd6125f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SimpleMetadataAwareAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SimpleMetadataAwareAspectInstanceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SingletonMetadataAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SingletonMetadataAwareAspectInstanceFactory.java index 4dc30e11310b..d45bcab90a79 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SingletonMetadataAwareAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SingletonMetadataAwareAspectInstanceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java index b5cf52470045..4f9573c2f779 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java @@ -3,9 +3,7 @@ * *

Normally to be used through an AspectJAutoProxyCreator rather than directly. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.aspectj.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java index 255bfe961ccb..90a016f34dfc 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,6 @@ protected void extendAdvisors(List candidateAdvisors) { @Override protected boolean shouldSkip(Class beanClass, String beanName) { - // TODO: Consider optimization by caching the list of the aspect names List candidateAdvisors = findCandidateAdvisors(); for (Advisor advisor : candidateAdvisors) { if (advisor instanceof AspectJPointcutAdvisor pointcutAdvisor && diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparator.java index 2d243fadc726..f98dce3cac2e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparator.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 a97f79cbb11f..dc2f33ffc07e 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AdviceEntry.java b/spring-aop/src/main/java/org/springframework/aop/config/AdviceEntry.java index 7d9b2ad2dc82..9d69fb833fb1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AdviceEntry.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AdviceEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AdvisorComponentDefinition.java b/spring-aop/src/main/java/org/springframework/aop/config/AdvisorComponentDefinition.java index 25c8fa2c4d3c..e7b5ca08877f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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/AdvisorEntry.java b/spring-aop/src/main/java/org/springframework/aop/config/AdvisorEntry.java index 1a8b45c4823f..43c389cef3c6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AdvisorEntry.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AdvisorEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java b/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java index 1bba8f1c2048..3247fa213d39 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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,14 +19,17 @@ 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.ProxyConfig; +import org.springframework.aop.framework.autoproxy.AutoProxyUtils; import org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator; import org.springframework.beans.factory.config.BeanDefinition; 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,69 +67,67 @@ 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); } public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { - if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { - BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); - definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); - } + defaultProxyConfig(registry).getPropertyValues().add("proxyTargetClass", Boolean.TRUE); } public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { - if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { - BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); - definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); + defaultProxyConfig(registry).getPropertyValues().add("exposeProxy", Boolean.TRUE); + } + + private static BeanDefinition defaultProxyConfig(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME)) { + return registry.getBeanDefinition(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME); } + RootBeanDefinition beanDefinition = new RootBeanDefinition(ProxyConfig.class); + beanDefinition.setSource(AopConfigUtils.class); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, beanDefinition); + return beanDefinition; } - @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"); if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { - BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); - if (!cls.getName().equals(apcDefinition.getBeanClassName())) { - int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); + BeanDefinition beanDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + if (!cls.getName().equals(beanDefinition.getBeanClassName())) { + int currentPriority = findPriorityForClass(beanDefinition.getBeanClassName()); int requiredPriority = findPriorityForClass(cls); if (currentPriority < requiredPriority) { - apcDefinition.setBeanClassName(cls.getName()); + beanDefinition.setBeanClassName(cls.getName()); } } return null; @@ -134,8 +135,8 @@ private static BeanDefinition registerOrEscalateApcAsRequired( RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); beanDefinition.setSource(source); - beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); return beanDefinition; } diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceHandler.java b/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceHandler.java index fa6cc80a1f3c..45b20a279033 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceHandler.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java b/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java index 5acb1cc5acd9..d9b297e4f6a8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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..0c489e2ce155 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..390cc3b227b4 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-present the original author 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..7b1fc9595641 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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.aop.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -25,7 +26,6 @@ import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; /** * {@link BeanDefinitionParser} for the {@code aspectj-autoproxy} tag, @@ -39,8 +39,7 @@ class AspectJAutoProxyBeanDefinitionParser implements BeanDefinitionParser { @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); extendBeanDefinition(element, parserContext); return null; diff --git a/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java index c13c6446a383..a8fc92f027a9 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-2024 the original author or authors. + * Copyright 2002-present the original author 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); @@ -453,8 +452,7 @@ private AbstractBeanDefinition parsePointcut(Element pointcutElement, ParserCont * {@link org.springframework.beans.factory.config.BeanDefinition} for the pointcut if necessary * and returns its bean name, otherwise returns the bean name of the referred pointcut. */ - @Nullable - private Object parsePointcutProperty(Element element, ParserContext parserContext) { + private @Nullable Object parsePointcutProperty(Element element, ParserContext parserContext) { if (element.hasAttribute(POINTCUT) && element.hasAttribute(POINTCUT_REF)) { parserContext.getReaderContext().error( "Cannot define both 'pointcut' and 'pointcut-ref' on tag.", diff --git a/spring-aop/src/main/java/org/springframework/aop/config/MethodLocatingFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/config/MethodLocatingFactoryBean.java index ebff6ee73e28..2c4ce434baed 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,12 @@ import java.lang.reflect.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..71a576f462b5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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.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/PointcutEntry.java b/spring-aop/src/main/java/org/springframework/aop/config/PointcutEntry.java index e6066c513ee9..b0d8d3bad02d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/PointcutEntry.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/PointcutEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java b/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java index d51e472b589c..db543695af58 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-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java index 446d5f93e0a8..f2c3ef8968a1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.aop.config; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.aspectj.AspectInstanceFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -35,11 +36,9 @@ */ public class SimpleBeanFactoryAwareAspectInstanceFactory implements AspectInstanceFactory, BeanFactoryAware { - @Nullable - private String aspectBeanName; + private @Nullable String aspectBeanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** @@ -69,8 +68,7 @@ public Object getAspectInstance() { } @Override - @Nullable - public ClassLoader getAspectClassLoader() { + public @Nullable ClassLoader getAspectClassLoader() { if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { return cbf.getBeanClassLoader(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java index f3adcd6a6718..4238c9f37783 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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.aop.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; @@ -23,7 +24,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; /** * {@link BeanDefinitionParser} responsible for parsing the @@ -52,8 +52,7 @@ class SpringConfiguredBeanDefinitionParser implements BeanDefinitionParser { @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { if (!parserContext.getRegistry().containsBeanDefinition(BEAN_CONFIGURER_ASPECT_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(); def.setBeanClassName(BEAN_CONFIGURER_ASPECT_CLASS_NAME); diff --git a/spring-aop/src/main/java/org/springframework/aop/config/package-info.java b/spring-aop/src/main/java/org/springframework/aop/config/package-info.java index b0d1010cb327..5fb98e99ed8e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/package-info.java @@ -2,9 +2,7 @@ * Support package for declarative AOP configuration, * with XML schema being the primary configuration format. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java index 1b021c7cd8d3..70f0c63122e6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,13 @@ import java.util.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; @@ -112,11 +112,13 @@ else if (advised.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE && if (isEligible(bean, beanName)) { ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); - if (!proxyFactory.isProxyTargetClass()) { + if (!proxyFactory.isProxyTargetClass() && !proxyFactory.hasUserSuppliedInterfaces()) { evaluateProxyInterfaces(bean.getClass(), proxyFactory); } proxyFactory.addAdvisor(this.advisor); customizeProxyFactory(proxyFactory); + proxyFactory.setFrozen(isFrozen()); + proxyFactory.setPreFiltered(true); // Use original ClassLoader if bean class not locally loaded in overriding class loader ClassLoader classLoader = getProxyClassLoader(); @@ -135,7 +137,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. @@ -187,6 +189,7 @@ protected boolean isEligible(Class targetClass) { protected ProxyFactory prepareProxyFactory(Object bean, String beanName) { ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); + proxyFactory.setFrozen(false); proxyFactory.setTarget(bean); return proxyFactory; } 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..80e48735d99a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 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..37876d18c868 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-present the original author 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 2aebe1688d7b..58a603167333 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 java.util.concurrent.ConcurrentHashMap; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.DynamicIntroductionAdvice; @@ -34,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; @@ -69,6 +70,8 @@ public class AdvisedSupport extends ProxyConfig implements Advised { /** use serialVersionUID from Spring 2.0 for interoperability. */ private static final long serialVersionUID = 2651364800145442165L; + private static final Advisor[] EMPTY_ADVISOR_ARRAY = new Advisor[0]; + /** * Canonical TargetSource when there's no target, and behavior is @@ -78,24 +81,28 @@ public class AdvisedSupport extends ProxyConfig implements Advised { /** Package-protected to allow direct access for efficiency. */ + @SuppressWarnings("serial") TargetSource targetSource = EMPTY_TARGET_SOURCE; /** Whether the Advisors are already filtered for the specific target class. */ private boolean preFiltered = false; /** The AdvisorChainFactory to use. */ + @SuppressWarnings("serial") private AdvisorChainFactory advisorChainFactory = DefaultAdvisorChainFactory.INSTANCE; /** * Interfaces to be implemented by the proxy. Held in List to keep the order * of registration, to create JDK proxy with specified order of interfaces. */ + @SuppressWarnings("serial") private List> interfaces = new ArrayList<>(); /** * List of Advisors. If an Advice is added, it will be wrapped * in an Advisor before being added to this List. */ + @SuppressWarnings("serial") private List advisors = new ArrayList<>(); /** @@ -104,15 +111,14 @@ public class AdvisedSupport extends ProxyConfig implements Advised { * @since 6.0.10 * @see #reduceToAdvisorKey */ + @SuppressWarnings("serial") private List advisorKey = this.advisors; /** Cache with Method as key and advisor chain List as value. */ - @Nullable - private transient Map> methodCache; + private transient @Nullable Map> methodCache; /** Cache with shared interceptors which are not method-specific. */ - @Nullable - private transient volatile List cachedInterceptors; + private transient volatile @Nullable List cachedInterceptors; /** * Optional field for {@link AopProxy} implementations to store metadata in. @@ -120,8 +126,7 @@ public class AdvisedSupport extends ProxyConfig implements Advised { * @since 6.1.3 * @see JdkDynamicAopProxy#JdkDynamicAopProxy(AdvisedSupport) */ - @Nullable - transient volatile Object proxyMetadataCache; + transient volatile @Nullable Object proxyMetadataCache; /** @@ -177,8 +182,7 @@ public void setTargetClass(@Nullable Class targetClass) { } @Override - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { return this.targetSource.getTargetClass(); } @@ -222,15 +226,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(); } } @@ -238,12 +242,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 @@ -252,19 +256,41 @@ 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() { - return this.advisors.toArray(new Advisor[0]); + return this.advisors.toArray(EMPTY_ADVISOR_ARRAY); } @Override @@ -488,20 +514,27 @@ public int countAdvicesOfType(@Nullable Class adviceClass) { * @return a List of MethodInterceptors (may also include InterceptorAndDynamicMethodMatchers) */ public List getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class targetClass) { - if (this.methodCache == null) { + 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). - List cachedInterceptors = this.cachedInterceptors; + cachedInterceptors = this.cachedInterceptors; if (cachedInterceptors == null) { cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice( this, method, targetClass); this.cachedInterceptors = cachedInterceptors; } - return cachedInterceptors; } - - // Method-specific cache for method-specific pointcuts - return this.methodCache.computeIfAbsent(new MethodCacheKey(method), k -> - this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(this, method, targetClass)); + return cachedInterceptors; } /** @@ -638,7 +671,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 @@ -674,11 +708,9 @@ private static final class AdvisorKeyEntry implements Advisor { private final Class adviceType; - @Nullable - private final String classFilterKey; + private final @Nullable String classFilterKey; - @Nullable - private final String methodMatcherKey; + private final @Nullable String methodMatcherKey; public AdvisorKeyEntry(Advisor advisor) { this.adviceType = advisor.getAdvice().getClass(); @@ -699,7 +731,7 @@ public Advice getAdvice() { } @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { return (this == other || (other instanceof AdvisorKeyEntry that && this.adviceType == that.adviceType && ObjectUtils.nullSafeEquals(this.classFilterKey, that.classFilterKey) && diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupportListener.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupportListener.java index c9f4dd733e5a..d7a165cd2c45 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupportListener.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupportListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisorChainFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisorChainFactory.java index 3d31b8c7d481..a7146026a7e7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.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/AopConfigException.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopConfigException.java index b58b0dd0c042..852ba8efaafe 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopConfigException.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopConfigException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java index 9653ced6bc8b..fbc63f1f94be 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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/AopInfrastructureBean.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopInfrastructureBean.java index 316833787b1f..5d78099c7303 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopInfrastructureBean.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopInfrastructureBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java index f103477504a1..f800bd387855 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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/AopProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyFactory.java index 6365ee3c0d42..a0519e46526c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java index 26651f6200b8..2285021d32dd 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,14 @@ import java.util.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 856d48538718..1efed81dec82 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,13 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.AopInvocationException; import org.springframework.aop.RawTargetAccess; import org.springframework.aop.TargetSource; import org.springframework.aop.support.AopUtils; +import org.springframework.aot.AotDetector; import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy; import org.springframework.cglib.core.CodeGenerationException; import org.springframework.cglib.core.GeneratorStrategy; @@ -51,7 +53,6 @@ import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.SmartClassLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -97,7 +98,7 @@ class CglibAopProxy implements AopProxy, Serializable { private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; - private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + private static final boolean COROUTINES_REACTOR_PRESENT = ClassUtils.isPresent( "kotlinx.coroutines.reactor.MonoKt", CglibAopProxy.class.getClassLoader()); private static final GeneratorStrategy undeclaredThrowableStrategy = @@ -113,11 +114,9 @@ class CglibAopProxy implements AopProxy, Serializable { /** The configuration used to configure this proxy. */ protected final AdvisedSupport advised; - @Nullable - protected Object[] constructorArgs; + protected Object @Nullable [] constructorArgs; - @Nullable - protected Class[] constructorArgTypes; + protected Class @Nullable [] constructorArgTypes; /** Dispatcher used for methods on Advised. */ private final transient AdvisedDispatcher advisedDispatcher; @@ -144,7 +143,7 @@ public CglibAopProxy(AdvisedSupport config) throws AopConfigException { * @param constructorArgs the constructor argument values * @param constructorArgTypes the constructor argument types */ - public void setConstructorArguments(@Nullable Object[] constructorArgs, @Nullable Class[] constructorArgTypes) { + public void setConstructorArguments(Object @Nullable [] constructorArgs, Class @Nullable [] constructorArgTypes) { if (constructorArgs == null || constructorArgTypes == null) { throw new IllegalArgumentException("Both 'constructorArgs' and 'constructorArgTypes' need to be specified"); } @@ -205,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, undeclaredThrowableStrategy)); + 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]; @@ -288,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: " + @@ -412,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 @@ -428,7 +435,7 @@ private static Object processReturnType( throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } - if (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(method)) { + if (COROUTINES_REACTOR_PRESENT && 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]); @@ -452,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); } @@ -474,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); @@ -511,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); @@ -539,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 { @@ -565,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; } } @@ -652,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) { @@ -667,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); @@ -692,14 +688,13 @@ 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; TargetSource targetSource = this.advised.getTargetSource(); try { - if (this.advised.exposeProxy) { + if (this.advised.isExposeProxy()) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; @@ -716,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); } @@ -753,26 +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 { - return super.proceed(); - } - } - - /** * 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 f1e06096161a..f0e56cab0fa1 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,10 @@ import kotlin.coroutines.Continuation; import kotlinx.coroutines.reactive.ReactiveFlowKt; import kotlinx.coroutines.reactor.MonoKt; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import org.springframework.lang.Nullable; - /** * Package-visible class designed to avoid a hard dependency on Kotlin and Coroutines dependency at runtime. * @@ -41,9 +40,8 @@ static Object asFlow(@Nullable Object publisher) { } } - @Nullable - @SuppressWarnings({"unchecked", "rawtypes"}) - static Object awaitSingleOrNull(@Nullable Object value, Object continuation) { + @SuppressWarnings({"rawtypes", "unchecked"}) + 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..1eb3d0d7d3aa 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..b732c535ebdc 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-present the original author 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/InterceptorAndDynamicMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/framework/InterceptorAndDynamicMethodMatcher.java index 41366d7feee6..d34ae2237a6d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/InterceptorAndDynamicMethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/InterceptorAndDynamicMethodMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java index 3ab70ee9e877..b0016b00c039 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.AopInvocationException; import org.springframework.aop.RawTargetAccess; @@ -35,7 +36,6 @@ import org.springframework.core.DecoratingProxy; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -75,7 +75,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; - private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + private static final boolean COROUTINES_REACTOR_PRESENT = ClassUtils.isPresent( "kotlinx.coroutines.reactor.MonoKt", JdkDynamicAopProxy.class.getClassLoader()); /** We use a static Log to avoid serialization issues. */ @@ -163,8 +163,7 @@ private ClassLoader determineClassLoader(@Nullable ClassLoader classLoader) { * unless a hook method throws an exception. */ @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; @@ -184,7 +183,7 @@ else if (method.getDeclaringClass() == DecoratingProxy.class) { // There is only getDecoratedClass() declared -> dispatch to proxy config. return AopProxyUtils.ultimateTargetClass(this.advised); } - else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + else if (!this.advised.isOpaque() && method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAssignableFrom(Advised.class)) { // Service invocations on ProxyConfig with the proxy config... return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); @@ -192,7 +191,7 @@ else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && Object retVal; - if (this.advised.exposeProxy) { + if (this.advised.isExposeProxy()) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; @@ -212,7 +211,7 @@ else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && // We can skip creating a MethodInvocation: just invoke the target directly // Note that the final invoker must be an InvokerInterceptor so we know it does // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. - Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + @Nullable Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { @@ -237,7 +236,7 @@ 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 (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(method)) { + if (COROUTINES_REACTOR_PRESENT && 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]); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java index df24ece09578..c516dac616ef 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 @@ /** * Objenesis-based extension of {@link CglibAopProxy} to create proxy instances - * without invoking the constructor of the class. Used by default as of Spring 4. + * without invoking the constructor of the class. Used by default. * * @author Oliver Gierke * @author Juergen Hoeller diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java index 3b6010f8f58c..ca21266ba0ec 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -34,15 +36,15 @@ public class ProxyConfig implements Serializable { private static final long serialVersionUID = -8409359707199703185L; - private boolean proxyTargetClass = false; + private @Nullable Boolean proxyTargetClass; - private boolean optimize = false; + private @Nullable Boolean optimize; - boolean opaque = false; + private @Nullable Boolean opaque; - boolean exposeProxy = false; + private @Nullable Boolean exposeProxy; - private boolean frozen = false; + private @Nullable Boolean frozen; /** @@ -65,7 +67,7 @@ public void setProxyTargetClass(boolean proxyTargetClass) { * Return whether to proxy the target class directly as well as any interfaces. */ public boolean isProxyTargetClass() { - return this.proxyTargetClass; + return (this.proxyTargetClass != null && this.proxyTargetClass); } /** @@ -85,7 +87,7 @@ public void setOptimize(boolean optimize) { * Return whether proxies should perform aggressive optimizations. */ public boolean isOptimize() { - return this.optimize; + return (this.optimize != null && this.optimize); } /** @@ -103,7 +105,7 @@ public void setOpaque(boolean opaque) { * prevented from being cast to {@link Advised}. */ public boolean isOpaque() { - return this.opaque; + return (this.opaque != null && this.opaque); } /** @@ -124,7 +126,7 @@ public void setExposeProxy(boolean exposeProxy) { * each invocation. */ public boolean isExposeProxy() { - return this.exposeProxy; + return (this.exposeProxy != null && this.exposeProxy); } /** @@ -141,7 +143,7 @@ public void setFrozen(boolean frozen) { * Return whether the config is frozen, and no advice changes can be made. */ public boolean isFrozen() { - return this.frozen; + return (this.frozen != null && this.frozen); } @@ -153,9 +155,34 @@ public void copyFrom(ProxyConfig other) { Assert.notNull(other, "Other ProxyConfig object must not be null"); this.proxyTargetClass = other.proxyTargetClass; this.optimize = other.optimize; + this.opaque = other.opaque; this.exposeProxy = other.exposeProxy; this.frozen = other.frozen; - this.opaque = other.opaque; + } + + /** + * Copy default settings from the other config object, + * for settings that have not been locally set. + * @param other object to copy configuration from + * @since 7.0 + */ + public void copyDefault(ProxyConfig other) { + Assert.notNull(other, "Other ProxyConfig object must not be null"); + if (this.proxyTargetClass == null) { + this.proxyTargetClass = other.proxyTargetClass; + } + if (this.optimize == null) { + this.optimize = other.optimize; + } + if (this.opaque == null) { + this.opaque = other.opaque; + } + if (this.exposeProxy == null) { + this.exposeProxy = other.exposeProxy; + } + if (this.frozen == null) { + this.frozen = other.frozen; + } } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java index a9b43befbc9b..b9d1e3435de1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java index 56330a6395a3..9e9418af357b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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,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..cb1257cd5cf2 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-present the original author 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..e6fecf622e62 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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..b5de86b7b7b3 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-present the original author 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/AdvisorAdapter.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapter.java index d717bbf19f6f..aa3674dfe4eb 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapter.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationManager.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationManager.java index 6589fffd19b0..ab51c46dea44 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationManager.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistry.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistry.java index 5a9fb9947a88..687e29a719f2 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistry.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceAdapter.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceAdapter.java index ba4b049e2bc4..0deb181aaca7 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceAdapter.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..1b1af96dc309 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,10 @@ import org.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/DefaultAdvisorAdapterRegistry.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java index e1a92b241d3b..4c15b8995378 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,9 @@ @SuppressWarnings("serial") public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable { + private static final MethodInterceptor[] EMPTY_METHOD_INTERCEPTOR_ARRAY = new MethodInterceptor[0]; + + private final List adapters = new ArrayList<>(3); @@ -89,7 +92,7 @@ public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdvice if (interceptors.isEmpty()) { throw new UnknownAdviceTypeException(advisor.getAdvice()); } - return interceptors.toArray(new MethodInterceptor[0]); + return interceptors.toArray(EMPTY_METHOD_INTERCEPTOR_ARRAY); } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/GlobalAdvisorAdapterRegistry.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/GlobalAdvisorAdapterRegistry.java index 705fe9467e86..8a5a8cb5df56 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/GlobalAdvisorAdapterRegistry.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/GlobalAdvisorAdapterRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceAdapter.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceAdapter.java index 7cd7262aec9a..c4e4aea4fc00 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceAdapter.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..b3e504293a6e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,10 @@ import org.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/ThrowsAdviceAdapter.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java index a6b09c63cbbe..86a77d4f4947 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..0bf42ee80bf0 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,10 @@ import org.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/UnknownAdviceTypeException.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/UnknownAdviceTypeException.java index 1f09b8e52ec6..01460ac86c9a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/UnknownAdviceTypeException.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/UnknownAdviceTypeException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..b6b4e80bce07 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-present the original author 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 599ccf6de9ed..e72485d266fe 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,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; @@ -43,12 +44,10 @@ import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; 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; @@ -71,7 +70,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. @@ -101,8 +100,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 @@ -118,22 +116,14 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport /** Default is global AdvisorAdapterRegistry. */ private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); - /** - * Indicates whether the proxy should be frozen. Overridden from super - * to prevent the configuration from becoming frozen too early. - */ - private boolean freezeProxy = false; - /** Default is no common interceptors. */ private String[] interceptorNames = new String[0]; private boolean applyCommonInterceptorsFirst = true; - @Nullable - private TargetSourceCreator[] customTargetSourceCreators; + private TargetSourceCreator @Nullable [] customTargetSourceCreators; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; private final Set targetSourcedBeans = ConcurrentHashMap.newKeySet(16); @@ -144,22 +134,6 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport private final Map advisedBeans = new ConcurrentHashMap<>(256); - /** - * Set whether the proxy should be frozen, preventing advice - * from being added to it once it is created. - *

Overridden from the superclass to prevent the proxy configuration - * from being frozen before the proxy is created. - */ - @Override - public void setFrozen(boolean frozen) { - this.freezeProxy = frozen; - } - - @Override - public boolean isFrozen() { - return this.freezeProxy; - } - /** * Specify the {@link AdvisorAdapterRegistry} to use. *

Default is the global {@link AdvisorAdapterRegistry}. @@ -209,21 +183,20 @@ public void setApplyCommonInterceptorsFirst(boolean applyCommonInterceptorsFirst @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; + AutoProxyUtils.applyDefaultProxyConfig(this, beanFactory); } /** * Return the owning {@link BeanFactory}. * May be {@code null}, as this post-processor doesn't need to belong to a bean factory. */ - @Nullable - protected BeanFactory getBeanFactory() { + protected @Nullable BeanFactory getBeanFactory() { return this.beanFactory; } @Override - @Nullable - public Class predictBeanType(Class beanClass, String beanName) { + public @Nullable Class predictBeanType(Class beanClass, String beanName) { if (this.proxyTypes.isEmpty()) { return null; } @@ -256,8 +229,7 @@ public Class determineBeanType(Class beanClass, String beanName) { } @Override - @Nullable - public Constructor[] determineCandidateConstructors(Class beanClass, String beanName) { + public Constructor @Nullable [] determineCandidateConstructors(Class beanClass, String beanName) { return null; } @@ -269,8 +241,7 @@ public Object getEarlyBeanReference(Object bean, String beanName) { } @Override - @Nullable - public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + public @Nullable Object postProcessBeforeInstantiation(Class beanClass, String beanName) { Object cacheKey = getCacheKey(beanClass, beanName); if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) { @@ -311,8 +282,7 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str * @see #getAdvicesAndAdvisorsForBean */ @Override - @Nullable - public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { + public @Nullable Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyBeanReferences.remove(cacheKey) != bean) { @@ -325,10 +295,8 @@ public Object postProcessAfterInitialization(@Nullable Object bean, String beanN /** * Build a cache key for the given bean class and bean name. - *

Note: As of 4.2.3, this implementation does not return a concatenated - * class/name String anymore but rather the most efficient cache key possible: - * a plain bean name, prepended with {@link BeanFactory#FACTORY_BEAN_PREFIX} - * in case of a {@code FactoryBean}; or if no bean name specified, then the + *

Note: As of 7.0.2, this implementation returns a composed cache key + * for bean class plus bean name; or if no bean name specified, then the * given bean {@code Class} as-is. * @param beanClass the bean class * @param beanName the bean name @@ -336,8 +304,7 @@ public Object postProcessAfterInitialization(@Nullable Object bean, String beanN */ protected Object getCacheKey(Class beanClass, @Nullable String beanName) { if (StringUtils.hasLength(beanName)) { - return (FactoryBean.class.isAssignableFrom(beanClass) ? - BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName); + return new ComposedCacheKey(beanClass, beanName); } else { return beanClass; @@ -403,7 +370,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. @@ -426,8 +393,7 @@ protected boolean shouldSkip(Class beanClass, String beanName) { * @return a TargetSource for this bean * @see #setCustomTargetSourceCreators */ - @Nullable - protected TargetSource getCustomTargetSource(Class beanClass, String beanName) { + protected @Nullable TargetSource getCustomTargetSource(Class beanClass, String beanName) { // We can't create fancy target sources for directly registered singletons. if (this.customTargetSourceCreators != null && this.beanFactory != null && this.beanFactory.containsBean(beanName)) { @@ -460,19 +426,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); @@ -480,6 +446,24 @@ private Object buildProxy(Class beanClass, @Nullable String beanName, ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); + proxyFactory.setFrozen(false); + + if (shouldProxyTargetClass(beanClass, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { + Class[] ifcs = (this.beanFactory instanceof ConfigurableListableBeanFactory clbf ? + AutoProxyUtils.determineExposedInterfaces(clbf, beanName) : null); + if (ifcs != null) { + proxyFactory.setProxyTargetClass(false); + for (Class ifc : ifcs) { + proxyFactory.addInterface(ifc); + } + } + if (ifcs != null ? ifcs.length == 0 : !proxyFactory.isProxyTargetClass()) { + evaluateProxyInterfaces(beanClass, proxyFactory); + } + } if (proxyFactory.isProxyTargetClass()) { // Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios) @@ -490,22 +474,13 @@ private Object buildProxy(Class beanClass, @Nullable String beanName, } } } - else { - // No proxyTargetClass flag enforced, let's apply our default checks... - if (shouldProxyTargetClass(beanClass, beanName)) { - proxyFactory.setProxyTargetClass(true); - } - else { - evaluateProxyInterfaces(beanClass, proxyFactory); - } - } Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); customizeProxyFactory(proxyFactory); - proxyFactory.setFrozen(this.freezeProxy); + proxyFactory.setFrozen(isFrozen()); if (advisorsPreFiltered()) { proxyFactory.setPreFiltered(true); } @@ -554,7 +529,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(); @@ -619,7 +594,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 @@ -633,8 +608,15 @@ 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; + + /** + * Composed cache key for bean class plus bean name. + * @see #getCacheKey(Class, String) + */ + private record ComposedCacheKey(Class beanClass, String beanName) { + } + } 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..256bdd5c9d56 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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}, - * adds exposure of the original target class for each proxied bean - * ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}), + * Extension of {@link AbstractAdvisingBeanPostProcessor} which implements + * {@link BeanFactoryAware}, adds exposure of the original target class for each + * proxied bean ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}), * and participates in an externally enforced target-class mode for any given bean * ({@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE}). * This post-processor is therefore aligned with {@link AbstractAutoProxyCreator}. @@ -40,13 +41,13 @@ public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends AbstractAdvisingBeanPostProcessor implements BeanFactoryAware { - @Nullable - private ConfigurableListableBeanFactory beanFactory; + protected @Nullable ConfigurableListableBeanFactory beanFactory; @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = (beanFactory instanceof ConfigurableListableBeanFactory clbf ? clbf : null); + AutoProxyUtils.applyDefaultProxyConfig(this, beanFactory); } @Override @@ -56,9 +57,19 @@ protected ProxyFactory prepareProxyFactory(Object bean, String beanName) { } ProxyFactory proxyFactory = super.prepareProxyFactory(bean, beanName); - if (!proxyFactory.isProxyTargetClass() && this.beanFactory != null && - AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) { - proxyFactory.setProxyTargetClass(true); + if (this.beanFactory != null) { + if (AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { + Class[] ifcs = AutoProxyUtils.determineExposedInterfaces(this.beanFactory, beanName); + if (ifcs != null) { + proxyFactory.setProxyTargetClass(false); + for (Class ifc : ifcs) { + proxyFactory.addInterface(ifc); + } + } + } } return proxyFactory; } 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..3522bfd8b668 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.aop.framework.autoproxy; +import org.jspecify.annotations.Nullable; + +import org.springframework.aop.framework.ProxyConfig; +import org.springframework.beans.factory.BeanFactory; 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; /** @@ -30,9 +33,37 @@ * @author Juergen Hoeller * @since 2.0.3 * @see AbstractAutoProxyCreator + * @see AbstractBeanFactoryAwareAdvisingPostProcessor */ public abstract class AutoProxyUtils { + /** + * The bean name of the internally managed auto-proxy creator. + * @since 7.0 + */ + public static final String DEFAULT_PROXY_CONFIG_BEAN_NAME = + "org.springframework.aop.framework.autoproxy.defaultProxyConfig"; + + /** + * Bean definition attribute that may indicate the interfaces to be proxied + * (in case of it getting proxied in the first place). The value is either + * a single interface {@code Class} or an array of {@code Class}, with an + * empty array specifically signalling that all implemented interfaces need + * to be proxied. + * @since 7.0 + * @see #determineExposedInterfaces + */ + public static final String EXPOSED_INTERFACES_ATTRIBUTE = + Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "exposedInterfaces"); + + /** + * Attribute value for specifically signalling that all implemented interfaces + * need to be proxied (through an empty {@code Class} array). + * @since 7.0 + * @see #EXPOSED_INTERFACES_ATTRIBUTE + */ + public static final Object ALL_INTERFACES_ATTRIBUTE_VALUE = new Class[0]; + /** * Bean definition attribute that may indicate whether a given bean is supposed * to be proxied with its target class (in case of it getting proxied in the first @@ -47,7 +78,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 @@ -56,6 +87,47 @@ public abstract class AutoProxyUtils { Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "originalTargetClass"); + /** + * Apply default ProxyConfig settings to the given ProxyConfig instance, if necessary. + * @param proxyConfig the current ProxyConfig instance + * @param beanFactory the BeanFactory to take the default ProxyConfig from + * @since 7.0 + * @see #DEFAULT_PROXY_CONFIG_BEAN_NAME + * @see ProxyConfig#copyDefault + */ + static void applyDefaultProxyConfig(ProxyConfig proxyConfig, BeanFactory beanFactory) { + if (beanFactory.containsBean(DEFAULT_PROXY_CONFIG_BEAN_NAME)) { + ProxyConfig defaultProxyConfig = beanFactory.getBean(DEFAULT_PROXY_CONFIG_BEAN_NAME, ProxyConfig.class); + proxyConfig.copyDefault(defaultProxyConfig); + } + } + + /** + * Determine the specific interfaces for proxying the given bean, if any. + * Checks the {@link #EXPOSED_INTERFACES_ATTRIBUTE "exposedInterfaces" attribute} + * of the corresponding bean definition. + * @param beanFactory the containing ConfigurableListableBeanFactory + * @param beanName the name of the bean + * @return whether the given bean should be proxied with its target class + * @since 7.0 + * @see #EXPOSED_INTERFACES_ATTRIBUTE + */ + static Class @Nullable [] determineExposedInterfaces( + ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) { + + if (beanName != null && beanFactory.containsBeanDefinition(beanName)) { + BeanDefinition bd = beanFactory.getBeanDefinition(beanName); + Object interfaces = bd.getAttribute(EXPOSED_INTERFACES_ATTRIBUTE); + if (interfaces instanceof Class[] ifcs) { + return ifcs; + } + else if (interfaces instanceof Class ifc) { + return new Class[] {ifc}; + } + } + return null; + } + /** * Determine whether the given bean should be proxied with its target * class rather than its interfaces. Checks the @@ -64,6 +136,7 @@ public abstract class AutoProxyUtils { * @param beanFactory the containing ConfigurableListableBeanFactory * @param beanName the name of the bean * @return whether the given bean should be proxied with its target class + * @see #PRESERVE_TARGET_CLASS_ATTRIBUTE */ public static boolean shouldProxyTargetClass( ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) { @@ -84,8 +157,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..5be86ca467f7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +21,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 c9ea561366a4..426c37dd77a5 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-present the original author 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,17 @@ 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". + * FactoryBean will get proxied. If you intend to proxy a FactoryBean instance + * itself (a rare use case), specify the bean name of the FactoryBean + * including the factory-bean prefix "&": for example, "&myFactoryBean". * @see org.springframework.beans.factory.FactoryBean * @see org.springframework.beans.factory.BeanFactory#FACTORY_BEAN_PREFIX */ @@ -81,8 +80,7 @@ public void setBeanNames(String... beanNames) { * @see #setBeanNames(String...) */ @Override - @Nullable - protected TargetSource getCustomTargetSource(Class beanClass, String beanName) { + protected @Nullable TargetSource getCustomTargetSource(Class beanClass, String beanName) { return (isSupportedBeanName(beanClass, beanName) ? super.getCustomTargetSource(beanClass, beanName) : null); } @@ -93,8 +91,7 @@ protected TargetSource getCustomTargetSource(Class beanClass, String beanName * @see #setBeanNames(String...) */ @Override - @Nullable - protected Object[] getAdvicesAndAdvisorsForBean( + protected Object @Nullable [] getAdvicesAndAdvisorsForBean( Class beanClass, String beanName, @Nullable TargetSource targetSource) { return (isSupportedBeanName(beanClass, beanName) ? @@ -114,10 +111,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..00ea629050ce 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..f46f61a17724 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..807d3d55278f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..64e53338c18b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.aop.framework.autoproxy; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; /** * Implementations can create special target sources, such as pooling target @@ -40,7 +41,6 @@ public interface TargetSourceCreator { * @return a special TargetSource or {@code null} if this TargetSourceCreator isn't * interested in the particular bean */ - @Nullable - TargetSource getTargetSource(Class beanClass, String beanName); + @Nullable TargetSource getTargetSource(Class beanClass, String beanName); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java index 328312146ade..15acaaff35b9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java @@ -9,9 +9,7 @@ * as post-processors beans are only automatically detected in application contexts. * Post-processors can be explicitly registered on a ConfigurableBeanFactory instead. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.framework.autoproxy; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java index b9a5e4e4c422..f9022edad127 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-2024 the original author or authors. + * Copyright 2002-present the original author 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) { @@ -195,8 +192,7 @@ protected boolean isPrototypeBased() { * @param beanName the name of the bean * @return the AbstractPrototypeBasedTargetSource, or {@code null} if we don't match this */ - @Nullable - protected abstract AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource( + protected abstract @Nullable AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource( Class beanClass, String beanName); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/LazyInitTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/LazyInitTargetSourceCreator.java index 68ca0524471a..ee45a6e40ff3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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..b835a6c5069a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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..8956aa5856d4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,7 @@ import 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..6b9068b5a4ba 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author 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,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 a9b63d90091e..8bd431825047 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-2023 the original author or authors. + * Copyright 2002-present the original author 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,7 +52,7 @@ *

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 @@ -78,11 +78,9 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { private SingletonSupplier exceptionHandler; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; private final Map executors = new ConcurrentHashMap<>(16); @@ -118,8 +116,8 @@ public AsyncExecutionAspectSupport(@Nullable Executor defaultExecutor, AsyncUnca * applying the corresponding default if a supplier is not resolvable. * @since 5.1 */ - public void configure(@Nullable Supplier defaultExecutor, - @Nullable Supplier exceptionHandler) { + public void configure(@Nullable Supplier defaultExecutor, + @Nullable Supplier exceptionHandler) { this.defaultExecutor = new SingletonSupplier<>(defaultExecutor, () -> getDefaultExecutor(this.beanFactory)); this.exceptionHandler = new SingletonSupplier<>(exceptionHandler, SimpleAsyncUncaughtExceptionHandler::new); @@ -167,8 +165,7 @@ public void setBeanFactory(BeanFactory beanFactory) { * Determine the specific executor to use when executing the given method. * @return the executor to use (or {@code null}, but just if no default executor is available) */ - @Nullable - protected AsyncTaskExecutor determineAsyncExecutor(Method method) { + protected @Nullable AsyncTaskExecutor determineAsyncExecutor(Method method) { AsyncTaskExecutor executor = this.executors.get(method); if (executor == null) { Executor targetExecutor; @@ -203,8 +200,7 @@ protected AsyncTaskExecutor determineAsyncExecutor(Method method) { * @see #determineAsyncExecutor(Method) * @see #findQualifiedExecutor(BeanFactory, String) */ - @Nullable - protected abstract String getExecutorQualifier(Method method); + protected abstract @Nullable String getExecutorQualifier(Method method); /** * Retrieve a target executor for the given qualifier. @@ -213,8 +209,7 @@ protected AsyncTaskExecutor determineAsyncExecutor(Method method) { * @since 4.2.6 * @see #getExecutorQualifier(Method) */ - @Nullable - protected Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, String qualifier) { + protected @Nullable Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, String qualifier) { if (beanFactory == null) { throw new IllegalStateException("BeanFactory must be set on " + getClass().getSimpleName() + " to access qualified executor '" + qualifier + "'"); @@ -234,8 +229,7 @@ protected Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, Stri * @see #findQualifiedExecutor(BeanFactory, String) * @see #DEFAULT_TASK_EXECUTOR_BEAN_NAME */ - @Nullable - protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { + protected @Nullable Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { if (beanFactory != null) { try { // Search for TaskExecutor bean... not plain Executor since that would @@ -281,15 +275,10 @@ protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { * @param returnType the declared return type (potentially a {@link Future} variant) * @return the execution result (potentially a corresponding {@link Future} handle) */ - @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); } @@ -315,7 +304,7 @@ else if (void.class == returnType || "kotlin.Unit".equals(returnType.getName())) * @param method the method that was invoked * @param params the parameters used to invoke the method */ - protected void handleError(Throwable ex, Method method, Object... params) throws Exception { + protected void handleError(Throwable ex, Method method, @Nullable Object... params) throws Exception { if (Future.class.isAssignableFrom(method.getReturnType())) { ReflectionUtils.rethrowException(ex); } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java index 049cd3b623cd..4925305c898a 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,7 +32,6 @@ import org.springframework.core.Ordered; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.lang.Nullable; /** * AOP Alliance {@code MethodInterceptor} that processes method invocations @@ -97,9 +97,7 @@ public AsyncExecutionInterceptor(@Nullable Executor defaultExecutor, AsyncUncaug * otherwise. */ @Override - @Nullable - @SuppressWarnings("NullAway") - public Object invoke(final MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(final MethodInvocation invocation) throws Throwable { Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); final Method userMethod = BridgeMethodResolver.getMostSpecificMethod(invocation.getMethod(), targetClass); @@ -117,7 +115,8 @@ public Object invoke(final MethodInvocation invocation) throws Throwable { } } catch (ExecutionException ex) { - handleError(ex.getCause(), userMethod, invocation.getArguments()); + Throwable cause = ex.getCause(); + handleError(cause == null ? ex : cause, userMethod, invocation.getArguments()); } catch (Throwable ex) { handleError(ex, userMethod, invocation.getArguments()); @@ -125,7 +124,7 @@ public Object invoke(final MethodInvocation invocation) throws Throwable { return null; }; - return doSubmit(task, executor, invocation.getMethod().getReturnType()); + return doSubmit(task, executor, userMethod.getReturnType()); } /** @@ -140,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..36aa4340724d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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.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..528cbdb9f0c2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author 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 @@ 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; /** @@ -30,8 +30,8 @@ * *

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). + * throttle concurrency for a specific service rather than restrict + * 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. @@ -44,13 +44,26 @@ public class ConcurrencyThrottleInterceptor extends ConcurrencyThrottleSupport implements MethodInterceptor, Serializable { + /** + * Create a default {@code ConcurrencyThrottleInterceptor} + * with concurrency limit 1. + */ public ConcurrencyThrottleInterceptor() { - setConcurrencyLimit(1); + this(1); } + /** + * Create a {@code ConcurrencyThrottleInterceptor} + * with the given concurrency limit. + * @since 7.0 + */ + public ConcurrencyThrottleInterceptor(int concurrencyLimit) { + setConcurrencyLimit(concurrencyLimit); + } + + @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..1ee4fbf517e4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,8 @@ import 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..4ec2ab82d46a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author 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,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..6fb43d889969 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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.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..38937596a56d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author 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,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..07b01498d30a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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.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..fc8628956e75 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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..f1d3157198c1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author 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.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/DefaultScopedObject.java b/spring-aop/src/main/java/org/springframework/aop/scope/DefaultScopedObject.java index 302cd9e995ca..935f0e2e838a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/DefaultScopedObject.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/DefaultScopedObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedObject.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedObject.java index ff5edbb9faef..92f520ebc583 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedObject.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java index 572e7305fb36..43f624b34eda 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-present the original author 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,9 +53,8 @@ class ScopedProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProc @Override - @Nullable - @SuppressWarnings("NullAway") - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + @SuppressWarnings("NullAway") // Lambda + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); if (beanClass.equals(ScopedProxyFactoryBean.class)) { String targetBeanName = getTargetBeanName(registeredBean.getMergedBeanDefinition()); @@ -73,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); @@ -124,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 a787a1ee809c..6b5eedd6b32a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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.Modifier; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.framework.ProxyConfig; import org.springframework.aop.framework.ProxyFactory; @@ -28,7 +30,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBeanNotInitializedException; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -59,12 +60,10 @@ public class ScopedProxyFactoryBean extends ProxyConfig private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource(); /** The name of the target bean. */ - @Nullable - private String targetBeanName; + private @Nullable String targetBeanName; /** The cached singleton proxy. */ - @Nullable - private Object proxy; + private @Nullable Object proxy; /** @@ -117,8 +116,7 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override - @Nullable - public Object getObject() { + public @Nullable Object getObject() { if (this.proxy == null) { throw new FactoryBeanNotInitializedException(); } @@ -126,8 +124,7 @@ public Object getObject() { } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { if (this.proxy != null) { return this.proxy.getClass(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java index 2eee3a42581e..32f68f2c7b1a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.scope; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.autoproxy.AutoProxyUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -23,7 +25,6 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -81,13 +82,19 @@ public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder defini // Copy autowire settings from original bean definition. proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); proxyDefinition.setPrimary(targetDefinition.isPrimary()); + proxyDefinition.setFallback(targetDefinition.isFallback()); if (targetDefinition instanceof AbstractBeanDefinition abd) { + proxyDefinition.setDefaultCandidate(abd.isDefaultCandidate()); proxyDefinition.copyQualifiersFrom(abd); } // The target bean should be ignored in favor of the scoped proxy. targetDefinition.setAutowireCandidate(false); targetDefinition.setPrimary(false); + targetDefinition.setFallback(false); + if (targetDefinition instanceof AbstractBeanDefinition abd) { + abd.setDefaultCandidate(false); + } // Register the target bean as separate bean in the factory. registry.registerBeanDefinition(targetBeanName, targetDefinition); diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java b/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java index 443f903968fb..2736df6ebf72 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java @@ -1,9 +1,7 @@ /** * Support for AOP-based scoping of target objects, with configurable backend. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.scope; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java index e6a10c621bf1..2e94710111c0 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,10 @@ import java.io.ObjectInputStream; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -42,14 +42,11 @@ @SuppressWarnings("serial") public abstract class AbstractBeanFactoryPointcutAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { - @Nullable - private String adviceBeanName; + private @Nullable String adviceBeanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private transient volatile Advice advice; + private transient volatile @Nullable Advice advice; private transient Object adviceMonitor = new Object(); @@ -69,8 +66,7 @@ public void setAdviceBeanName(@Nullable String adviceBeanName) { /** * Return the name of the advice bean that this advisor refers to, if any. */ - @Nullable - public String getAdviceBeanName() { + public @Nullable String getAdviceBeanName() { return this.adviceBeanName; } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractExpressionPointcut.java index 5330f2c00d64..7e879c120235 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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/AbstractGenericPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractGenericPointcutAdvisor.java index 62432231ffe5..d7c5a78b578e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AbstractGenericPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractGenericPointcutAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java index fc5527270ed8..c9c7b344adc5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..4918941b0c6d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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 c2f4cccf3db8..9138d1c32a39 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.aop.support; +import java.lang.reflect.InaccessibleObjectException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -28,6 +29,7 @@ import kotlin.coroutines.Continuation; import kotlin.coroutines.CoroutineContext; import kotlinx.coroutines.Job; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.AopInvocationException; @@ -43,7 +45,6 @@ import org.springframework.core.KotlinDetector; import org.springframework.core.MethodIntrospector; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -65,7 +66,7 @@ */ public abstract class AopUtils { - private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + private static final boolean COROUTINES_REACTOR_PRESENT = ClassUtils.isPresent( "kotlinx.coroutines.reactor.MonoKt", AopUtils.class.getClassLoader()); @@ -194,7 +195,7 @@ 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}, @@ -347,15 +348,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 (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(method) ? - KotlinDelegate.invokeSuspendingFunction(method, target, args) : method.invoke(target, args)); + Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); + ReflectionUtils.makeAccessible(originalMethod); + return (COROUTINES_REACTOR_PRESENT && KotlinDetector.isSuspendingFunction(originalMethod) ? + KotlinDelegate.invokeSuspendingFunction(originalMethod, target, args) : originalMethod.invoke(target, args)); } catch (InvocationTargetException ex) { // Invoked method threw a checked exception. @@ -366,7 +367,7 @@ public static Object invokeJoinpointUsingReflection(@Nullable Object target, Met throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" + method + "] on target [" + target + "]", ex); } - catch (IllegalAccessException ex) { + catch (IllegalAccessException | InaccessibleObjectException ex) { throw new AopInvocationException("Could not access method [" + method + "]", ex); } } @@ -377,7 +378,7 @@ public static Object invokeJoinpointUsingReflection(@Nullable Object target, Met */ private static class KotlinDelegate { - public static Object invokeSuspendingFunction(Method method, @Nullable Object target, Object... args) { + public static Object invokeSuspendingFunction(Method method, @Nullable Object target, @Nullable Object... args) { Continuation continuation = (Continuation) args[args.length -1]; Assert.state(continuation != null, "No Continuation available"); CoroutineContext context = continuation.getContext().minusKey(Job.Key); diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java b/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java index 929196e66b74..f5a5c1290a70 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-present the original author 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; @@ -197,9 +198,9 @@ public boolean matches(Class clazz) { } @Override - public boolean equals(Object other) { - return (this == other || (other instanceof NegateClassFilter that - && this.original.equals(that.original))); + public boolean equals(@Nullable Object other) { + 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..572c5e057e83 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java index 43707df5c200..c5f766be7220 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; @@ -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..77aefd0e0d49 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..4cb1baf3c544 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +21,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..8a2c8fdf471c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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,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..b1fe7913e20e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,11 @@ import java.util.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; /** @@ -82,12 +82,11 @@ public DelegatePerTargetObjectIntroductionInterceptor(Class defaultImplType, /** * Subclasses may need to override this if they want to perform custom - * behaviour in around advice. However, subclasses should invoke this + * behavior in around advice. However, subclasses should invoke this * 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..ffdd4c31630e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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,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; /** @@ -98,12 +97,11 @@ private void init(Object delegate) { /** * Subclasses may need to override this if they want to perform custom - * behaviour in around advice. However, subclasses should invoke this + * behavior in around advice. However, subclasses should invoke this * 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/DynamicMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcher.java index d45e1d992add..3e1d6a116a42 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcherPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcherPointcut.java index d25972434f6f..5f93bfad3f8c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcherPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcherPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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 @@ * Convenient superclass when we want to force subclasses to * implement MethodMatcher interface, but subclasses * will want to be pointcuts. The getClassFilter() method can - * be overridden to customize ClassFilter behaviour as well. + * be overridden to customize ClassFilter behavior as well. * * @author Rod Johnson */ 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..2487bdfc488b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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/IntroductionInfoSupport.java b/spring-aop/src/main/java/org/springframework/aop/support/IntroductionInfoSupport.java index 1033375bb7bf..f51d56160ebc 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/IntroductionInfoSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/IntroductionInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/support/JdkRegexpMethodPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/JdkRegexpMethodPointcut.java index 162b5cb31063..a00fb989876e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/JdkRegexpMethodPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/JdkRegexpMethodPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java b/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java index f2d226adfb28..f059efa375cc 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-present the original author 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))); + public boolean equals(@Nullable Object other) { + 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..2a3e1d98a8d3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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/NameMatchMethodPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcutAdvisor.java index a7efd815dc5f..26487c808855 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcutAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.java b/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.java index 35f3f644f54c..74ffb61f83d4 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/support/RegexpMethodPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/RegexpMethodPointcutAdvisor.java index bc41a9fbdd8d..2bfd04c01b7d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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,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..dde60ca6245f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..7413d8d76602 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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/StaticMethodMatcherPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcut.java index 1bae02698161..f3f309d36316 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcutAdvisor.java index f5b3ccdeaa79..6d5392257095 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcutAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..7fc39212acf8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,10 @@ import java.lang.annotation.Annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java index c6e11ecf77ae..bef8974f59f2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,12 @@ import java.lang.annotation.Annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -87,7 +88,7 @@ public AnnotationMatchingPointcut(@Nullable Class classAnn * @see AnnotationClassFilter#AnnotationClassFilter(Class, boolean) * @see AnnotationMethodMatcher#AnnotationMethodMatcher(Class, boolean) */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public AnnotationMatchingPointcut(@Nullable Class classAnnotationType, @Nullable Class methodAnnotationType, boolean checkInherited) { diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java index 520519ff7243..4f5eab21df8d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.lang.reflect.Proxy; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.StaticMethodMatcher; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java index a5ec1d421ab5..367eb885438d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Annotation support for AOP pointcuts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.support.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/package-info.java b/spring-aop/src/main/java/org/springframework/aop/support/package-info.java index a39f2d4c302c..af967794b426 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/package-info.java @@ -1,9 +1,7 @@ /** * Convenience classes for using Spring's AOP API. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java index 6ab195c0a7bd..912efdd9677f 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-present the original author 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; + protected @Nullable String targetBeanName; /** Class of the target. */ - @Nullable - private volatile Class targetClass; + private volatile @Nullable Class targetClass; /** * BeanFactory that owns this TargetSource. We need to hold onto this * reference so that we can create new prototype instances as necessary. */ - @Nullable - private BeanFactory beanFactory; + @SuppressWarnings("serial") + private @Nullable BeanFactory beanFactory; /** @@ -128,8 +126,7 @@ public BeanFactory getBeanFactory() { @Override - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { Class targetClass = this.targetClass; if (targetClass != null) { return targetClass; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java index 57838757015d..832b4a6d7261 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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; /** * {@link org.springframework.aop.TargetSource} implementation that will @@ -46,8 +46,7 @@ public abstract class AbstractLazyCreationTargetSource implements TargetSource { protected final Log logger = LogFactory.getLog(getClass()); /** The lazily initialized target object. */ - @Nullable - private Object lazyTarget; + private @Nullable Object lazyTarget; /** @@ -67,8 +66,7 @@ public synchronized boolean isInitialized() { * @see #isInitialized() */ @Override - @Nullable - public synchronized Class getTargetClass() { + public synchronized @Nullable Class getTargetClass() { return (this.lazyTarget != null ? this.lazyTarget.getClass() : null); } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java index 30e9c1e2ef23..efa9051b7453 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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.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/AbstractPrototypeBasedTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractPrototypeBasedTargetSource.java index 0824efc4678b..3051754c483f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/AbstractPrototypeBasedTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/AbstractPrototypeBasedTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 void setBeanFactory(BeanFactory beanFactory) throws BeansException { if (!beanFactory.isPrototype(getTargetBeanName())) { throw new BeanDefinitionStoreException( "Cannot use prototype-based TargetSource against non-prototype bean with name '" + - getTargetBeanName() + "': instances would not be independent"); + this.targetBeanName + "': instances would not be independent"); } } @@ -64,7 +64,7 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException { */ protected Object newPrototypeInstance() throws BeansException { if (logger.isDebugEnabled()) { - logger.debug("Creating new instance of bean '" + getTargetBeanName() + "'"); + logger.debug("Creating new instance of bean '" + this.targetBeanName + "'"); } return getBeanFactory().getBean(getTargetBeanName()); } @@ -75,7 +75,7 @@ protected Object newPrototypeInstance() throws BeansException { */ protected void destroyPrototypeInstance(Object target) { if (logger.isDebugEnabled()) { - logger.debug("Destroying instance of bean '" + getTargetBeanName() + "'"); + logger.debug("Destroying instance of bean '" + this.targetBeanName + "'"); } if (getBeanFactory() instanceof ConfigurableBeanFactory cbf) { cbf.destroyBean(getTargetBeanName(), target); @@ -85,7 +85,7 @@ else if (target instanceof DisposableBean disposableBean) { disposableBean.destroy(); } catch (Throwable ex) { - logger.warn("Destroy method on bean with name '" + getTargetBeanName() + "' threw an exception", ex); + logger.warn("Destroy method on bean with name '" + this.targetBeanName + "' threw an exception", ex); } } } 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..6654b080f9bf 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,8 @@ import 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; /** @@ -46,8 +46,6 @@ * meaningful validation. All exposed Commons Pool properties use the * corresponding Commons Pool defaults. * - *

Compatible with Apache Commons Pool 2.4, as of Spring 4.2. - * * @author Rod Johnson * @author Rob Harrop * @author Juergen Hoeller @@ -63,7 +61,7 @@ * @see #setTimeBetweenEvictionRunsMillis * @see #setMinEvictableIdleTimeMillis */ -@SuppressWarnings({"rawtypes", "unchecked", "serial"}) +@SuppressWarnings({"rawtypes", "unchecked", "serial", "deprecation"}) public class CommonsPool2TargetSource extends AbstractPoolingTargetSource implements PooledObjectFactory { private int maxIdle = GenericObjectPoolConfig.DEFAULT_MAX_IDLE; @@ -81,8 +79,7 @@ public class CommonsPool2TargetSource extends AbstractPoolingTargetSource implem /** * The Apache Commons {@code ObjectPool} used to pool target objects. */ - @Nullable - private ObjectPool pool; + private @Nullable ObjectPool pool; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java index cfcb3b119fdf..8460d4103046 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -71,8 +72,7 @@ public static EmptyTargetSource forClass(@Nullable Class targetClass, boolean // Instance implementation //--------------------------------------------------------------------- - @Nullable - private final Class targetClass; + private final @Nullable Class targetClass; private final boolean isStatic; @@ -94,8 +94,7 @@ private EmptyTargetSource(@Nullable Class targetClass, boolean isStatic) { * Always returns the specified target Class, or {@code null} if none. */ @Override - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { return this.targetClass; } @@ -111,8 +110,7 @@ public boolean isStatic() { * Always returns {@code null}. */ @Override - @Nullable - public Object getTarget() { + public @Nullable Object getTarget() { return null; } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java index fb5aceefcadf..161bf92d8f8b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/target/LazyInitTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/LazyInitTargetSource.java index e69a01842e00..643f95bf402a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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/PoolingConfig.java b/spring-aop/src/main/java/org/springframework/aop/target/PoolingConfig.java index bd439d95d728..b0025997f493 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/PoolingConfig.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/PoolingConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/target/PrototypeTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/PrototypeTargetSource.java index e43fb6a12ee9..edfb435943eb 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/PrototypeTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/PrototypeTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author 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 void releaseTarget(Object target) { @Override public String toString() { - return "PrototypeTargetSource for target bean with name '" + getTargetBeanName() + "'"; + return "PrototypeTargetSource for target bean with name '" + this.targetBeanName + "'"; } } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/SimpleBeanTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/SimpleBeanTargetSource.java index 6446df2d65fc..4f83b003e838 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/SimpleBeanTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/SimpleBeanTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java index 11e3a9e8fead..5422682d91ed 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSource.java index 7edfc0aff608..a01bc446b7e1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ public class ThreadLocalTargetSource extends AbstractPrototypeBasedTargetSource new NamedThreadLocal<>("Thread-local instance of bean") { @Override public String toString() { - return super.toString() + " '" + getTargetBeanName() + "'"; + return super.toString() + " '" + targetBeanName + "'"; } }; @@ -86,7 +86,7 @@ public Object getTarget() throws BeansException { Object target = this.targetInThread.get(); if (target == null) { if (logger.isDebugEnabled()) { - logger.debug("No target for prototype '" + getTargetBeanName() + "' bound to thread: " + + logger.debug("No target for prototype '" + this.targetBeanName + "' bound to thread: " + "creating one and binding it to thread '" + Thread.currentThread().getName() + "'"); } // Associate target with ThreadLocal. diff --git a/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSourceStats.java b/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSourceStats.java index 99405f629280..a9bdeeb7dc67 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSourceStats.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSourceStats.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java index 5871ac8b95d1..b7754b97aadf 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-2023 the original author or authors. + * Copyright 2002-present the original author 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,8 +42,7 @@ public abstract class AbstractRefreshableTargetSource implements TargetSource, R /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - protected Object targetObject; + protected @Nullable Object targetObject; private long refreshCheckDelay = -1; @@ -66,17 +65,15 @@ public void setRefreshCheckDelay(long refreshCheckDelay) { @Override - @SuppressWarnings("NullAway") - public synchronized Class getTargetClass() { + public synchronized @Nullable Class getTargetClass() { if (this.targetObject == null) { refresh(); } - return this.targetObject.getClass(); + return (this.targetObject != null ? this.targetObject.getClass() : null); } @Override - @Nullable - public final synchronized Object getTarget() { + public final synchronized @Nullable Object getTarget() { if ((refreshCheckDelayElapsed() && requiresRefresh()) || this.targetObject == null) { refresh(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/BeanFactoryRefreshableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/BeanFactoryRefreshableTargetSource.java index 0d5988f6b52b..772a43dacbc1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/BeanFactoryRefreshableTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/BeanFactoryRefreshableTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/Refreshable.java b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/Refreshable.java index b03bb5544a6d..58317fcb9b8a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/Refreshable.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/Refreshable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/package-info.java b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/package-info.java index 5ac4c66c1820..27d8af4ff16f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/package-info.java @@ -2,9 +2,7 @@ * Support for dynamic, refreshable {@link org.springframework.aop.TargetSource} * implementations for use with Spring AOP. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.target.dynamic; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/package-info.java b/spring-aop/src/main/java/org/springframework/aop/target/package-info.java index 292cdcce5d1e..88fb11976c00 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/package-info.java @@ -2,9 +2,7 @@ * Various {@link org.springframework.aop.TargetSource} implementations for use * with Spring AOP. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.target; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java new file mode 100644 index 000000000000..70c2f5a33584 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AbstractAspectJAdviceTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 d361600e6ea5..451229f55eff 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index 0e93dafdf462..436e2d78f0a0 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import test.annotation.EmptySpringAnnotation; import test.annotation.transaction.Tx; @@ -37,6 +36,7 @@ import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.subpkg.DeepBean; +import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -50,30 +50,22 @@ */ class AspectJExpressionPointcutTests { - private Method getAge; - - private Method setAge; - - private Method setSomeNumber; - + private final Method getAge = ClassUtils.getMethod(TestBean.class, "getAge"); + private final Method setAge = ClassUtils.getMethod(TestBean.class, "setAge", int.class); + private final Method setSomeNumber = ClassUtils.getMethod(TestBean.class, "setSomeNumber", Number.class); private final Map methodsOnHasGeneric = new HashMap<>(); - @BeforeEach - void setup() throws NoSuchMethodException { - getAge = TestBean.class.getMethod("getAge"); - setAge = TestBean.class.getMethod("setAge", int.class); - setSomeNumber = TestBean.class.getMethod("setSomeNumber", Number.class); - + AspectJExpressionPointcutTests() throws NoSuchMethodException { // Assumes no overloading for (Method method : HasGeneric.class.getMethods()) { - methodsOnHasGeneric.put(method.getName(), method); + this.methodsOnHasGeneric.put(method.getName(), method); } } @Test - void testMatchExplicit() { + void matchExplicit() { String expression = "execution(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; Pointcut pointcut = getPointcut(expression); @@ -91,7 +83,7 @@ void testMatchExplicit() { } @Test - void testMatchWithTypePattern() { + void matchWithTypePattern() { String expression = "execution(* *..TestBean.*Age(..))"; Pointcut pointcut = getPointcut(expression); @@ -110,12 +102,12 @@ void testMatchWithTypePattern() { @Test - void testThis() throws SecurityException, NoSuchMethodException{ + void thisCase() throws SecurityException, NoSuchMethodException{ testThisOrTarget("this"); } @Test - void testTarget() throws SecurityException, NoSuchMethodException { + void target() throws SecurityException, NoSuchMethodException { testThisOrTarget("target"); } @@ -139,12 +131,12 @@ private void testThisOrTarget(String which) throws SecurityException, NoSuchMeth } @Test - void testWithinRootPackage() throws SecurityException, NoSuchMethodException { + void withinRootPackage() throws SecurityException, NoSuchMethodException { testWithinPackage(false); } @Test - void testWithinRootAndSubpackages() throws SecurityException, NoSuchMethodException { + void withinRootAndSubpackages() throws SecurityException, NoSuchMethodException { testWithinPackage(true); } @@ -168,7 +160,7 @@ private void testWithinPackage(boolean matchSubpackages) throws SecurityExceptio } @Test - void testFriendlyErrorOnNoLocationClassMatching() { + void friendlyErrorOnNoLocationClassMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); assertThatIllegalStateException() .isThrownBy(() -> pc.getClassFilter().matches(ITestBean.class)) @@ -176,7 +168,7 @@ void testFriendlyErrorOnNoLocationClassMatching() { } @Test - void testFriendlyErrorOnNoLocation2ArgMatching() { + void friendlyErrorOnNoLocation2ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); assertThatIllegalStateException() .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class)) @@ -184,7 +176,7 @@ void testFriendlyErrorOnNoLocation2ArgMatching() { } @Test - void testFriendlyErrorOnNoLocation3ArgMatching() { + void friendlyErrorOnNoLocation3ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); assertThatIllegalStateException() .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class, (Object[]) null)) @@ -193,7 +185,7 @@ void testFriendlyErrorOnNoLocation3ArgMatching() { @Test - void testMatchWithArgs() { + void matchWithArgs() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number)) && args(Double)"; Pointcut pointcut = getPointcut(expression); @@ -214,7 +206,7 @@ void testMatchWithArgs() { } @Test - void testSimpleAdvice() { + void simpleAdvice() { String expression = "execution(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; CallCountingInterceptor interceptor = new CallCountingInterceptor(); TestBean testBean = getAdvisedProxy(expression, interceptor); @@ -227,7 +219,7 @@ void testSimpleAdvice() { } @Test - void testDynamicMatchingProxy() { + void dynamicMatchingProxy() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number)) && args(Double)"; CallCountingInterceptor interceptor = new CallCountingInterceptor(); TestBean testBean = getAdvisedProxy(expression, interceptor); @@ -241,7 +233,7 @@ void testDynamicMatchingProxy() { } @Test - void testInvalidExpression() { + void invalidExpression() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)"; assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } @@ -271,20 +263,20 @@ private void assertMatchesTestBeanClass(ClassFilter classFilter) { } @Test - void testWithUnsupportedPointcutPrimitive() { + void withUnsupportedPointcutPrimitive() { String expression = "call(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } @Test - void testAndSubstitution() { + void andSubstitution() { AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String)"); String expr = pc.getPointcutExpression().getPointcutExpression(); assertThat(expr).isEqualTo("execution(* *(..)) && args(String)"); } @Test - void testMultipleAndSubstitutions() { + void multipleAndSubstitutions() { AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); String expr = pc.getPointcutExpression().getPointcutExpression(); assertThat(expr).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); @@ -297,7 +289,7 @@ private AspectJExpressionPointcut getPointcut(String expression) { } @Test - void testMatchGenericArgument() { + void matchGenericArgument() { String expression = "execution(* set*(java.util.List) )"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -316,7 +308,7 @@ void testMatchGenericArgument() { } @Test - void testMatchVarargs() throws Exception { + void matchVarargs() throws Exception { @SuppressWarnings("unused") class MyTemplate { @@ -342,19 +334,19 @@ public int queryForInt(String sql, Object... params) { } @Test - void testMatchAnnotationOnClassWithAtWithin() throws Exception { + void matchAnnotationOnClassWithAtWithin() throws Exception { String expression = "@within(test.annotation.transaction.Tx)"; testMatchAnnotationOnClass(expression); } @Test - void testMatchAnnotationOnClassWithoutBinding() throws Exception { + void matchAnnotationOnClassWithoutBinding() throws Exception { String expression = "within(@test.annotation.transaction.Tx *)"; testMatchAnnotationOnClass(expression); } @Test - void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception { + void matchAnnotationOnClassWithSubpackageWildcard() throws Exception { String expression = "within(@(test.annotation..*) *)"; AspectJExpressionPointcut springAnnotatedPc = testMatchAnnotationOnClass(expression); assertThat(springAnnotatedPc.matches(TestBean.class.getMethod("setName", String.class), TestBean.class)).isFalse(); @@ -366,7 +358,7 @@ void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception { } @Test - void testMatchAnnotationOnClassWithExactPackageWildcard() throws Exception { + void matchAnnotationOnClassWithExactPackageWildcard() throws Exception { String expression = "within(@(test.annotation.transaction.*) *)"; testMatchAnnotationOnClass(expression); } @@ -384,7 +376,7 @@ private AspectJExpressionPointcut testMatchAnnotationOnClass(String expression) } @Test - void testAnnotationOnMethodWithFQN() throws Exception { + void annotationOnMethodWithFQN() throws Exception { String expression = "@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -398,7 +390,7 @@ void testAnnotationOnMethodWithFQN() throws Exception { } @Test - void testAnnotationOnCglibProxyMethod() throws Exception { + void annotationOnCglibProxyMethod() throws Exception { String expression = "@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -410,7 +402,7 @@ void testAnnotationOnCglibProxyMethod() throws Exception { } @Test - void testNotAnnotationOnCglibProxyMethod() throws Exception { + void notAnnotationOnCglibProxyMethod() throws Exception { String expression = "!@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -422,7 +414,7 @@ void testNotAnnotationOnCglibProxyMethod() throws Exception { } @Test - void testAnnotationOnDynamicProxyMethod() throws Exception { + void annotationOnDynamicProxyMethod() throws Exception { String expression = "@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -434,7 +426,7 @@ void testAnnotationOnDynamicProxyMethod() throws Exception { } @Test - void testNotAnnotationOnDynamicProxyMethod() throws Exception { + void notAnnotationOnDynamicProxyMethod() throws Exception { String expression = "!@annotation(test.annotation.transaction.Tx)"; AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(expression); @@ -446,7 +438,7 @@ void testNotAnnotationOnDynamicProxyMethod() throws Exception { } @Test - void testAnnotationOnMethodWithWildcard() throws Exception { + void annotationOnMethodWithWildcard() throws Exception { String expression = "execution(@(test.annotation..*) * *(..))"; AspectJExpressionPointcut anySpringMethodAnnotation = new AspectJExpressionPointcut(); anySpringMethodAnnotation.setExpression(expression); @@ -462,7 +454,7 @@ void testAnnotationOnMethodWithWildcard() throws Exception { } @Test - void testAnnotationOnMethodArgumentsWithFQN() throws Exception { + void annotationOnMethodArgumentsWithFQN() throws Exception { String expression = "@args(*, test.annotation.EmptySpringAnnotation))"; AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); takesSpringAnnotatedArgument2.setExpression(expression); @@ -491,7 +483,7 @@ void testAnnotationOnMethodArgumentsWithFQN() throws Exception { } @Test - void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { + void annotationOnMethodArgumentsWithWildcards() throws Exception { String expression = "execution(* *(*, @(test..*) *))"; AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); takesSpringAnnotatedArgument2.setExpression(expression); 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 3d61e242897b..a74adc9f64f3 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class BeanNamePointcutMatchingTests { @Test - void testMatchingPointcuts() { + void matchingPointcuts() { assertMatch("someName", "bean(someName)"); // Spring bean names are less restrictive compared to AspectJ names (methods, types etc.) @@ -66,7 +66,7 @@ void testMatchingPointcuts() { } @Test - void testNonMatchingPointcuts() { + void nonMatchingPointcuts() { assertMisMatch("someName", "bean(someNamex)"); assertMisMatch("someName", "bean(someX*Name)"); 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 50559b43a2e6..dbe3a7679ec3 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.aop.aspectj; import java.io.IOException; -import java.util.Arrays; import java.util.concurrent.atomic.AtomicInteger; import org.aspectj.lang.JoinPoint; @@ -49,17 +48,17 @@ class MethodInvocationProceedingJoinPointTests { @Test - void testingBindingWithJoinPoint() { + void bindingWithJoinPoint() { assertThatIllegalStateException().isThrownBy(AbstractAspectJAdvice::currentJoinPoint); } @Test - void testingBindingWithProceedingJoinPoint() { + void bindingWithProceedingJoinPoint() { assertThatIllegalStateException().isThrownBy(AbstractAspectJAdvice::currentJoinPoint); } @Test - void testCanGetMethodSignatureFromJoinPoint() { + void canGetMethodSignatureFromJoinPoint() { final Object raw = new TestBean(); // Will be set by advice during a method call final int newAge = 23; @@ -106,9 +105,9 @@ void testCanGetMethodSignatureFromJoinPoint() { assertThat(AbstractAspectJAdvice.currentJoinPoint().getSignature()).as("Return same MethodSignature repeatedly").isSameAs(msig); assertThat(AbstractAspectJAdvice.currentJoinPoint()).as("Return same JoinPoint repeatedly").isSameAs(AbstractAspectJAdvice.currentJoinPoint()); assertThat(msig.getDeclaringType()).isEqualTo(method.getDeclaringClass()); - assertThat(Arrays.equals(method.getParameterTypes(), msig.getParameterTypes())).isTrue(); + assertThat(method.getParameterTypes()).isEqualTo(msig.getParameterTypes()); assertThat(msig.getReturnType()).isEqualTo(method.getReturnType()); - assertThat(Arrays.equals(method.getExceptionTypes(), msig.getExceptionTypes())).isTrue(); + assertThat(method.getExceptionTypes()).isEqualTo(msig.getExceptionTypes()); msig.toLongString(); msig.toShortString(); }); @@ -118,7 +117,7 @@ void testCanGetMethodSignatureFromJoinPoint() { } @Test - void testCanGetSourceLocationFromJoinPoint() { + void canGetSourceLocationFromJoinPoint() { final Object raw = new TestBean(); ProxyFactory pf = new ProxyFactory(raw); pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); @@ -135,7 +134,7 @@ void testCanGetSourceLocationFromJoinPoint() { } @Test - void testCanGetStaticPartFromJoinPoint() { + void canGetStaticPartFromJoinPoint() { 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 d7dec7bf33d1..e0452e03a83f 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; @@ -43,14 +43,14 @@ class TrickyAspectJPointcutExpressionTests { @Test - void testManualProxyJavaWithUnconditionalPointcut() { + void manualProxyJavaWithUnconditionalPointcut() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); testAdvice(new DefaultPointcutAdvisor(logAdvice), logAdvice, target, "TestServiceImpl"); } @Test - void testManualProxyJavaWithStaticPointcut() { + void manualProxyJavaWithStaticPointcut() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -59,7 +59,7 @@ void testManualProxyJavaWithStaticPointcut() { } @Test - void testManualProxyJavaWithDynamicPointcut() { + void manualProxyJavaWithDynamicPointcut() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -68,7 +68,7 @@ void testManualProxyJavaWithDynamicPointcut() { } @Test - void testManualProxyJavaWithDynamicPointcutAndProxyTargetClass() { + void manualProxyJavaWithDynamicPointcutAndProxyTargetClass() { TestService target = new TestServiceImpl(); LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); @@ -77,7 +77,7 @@ void testManualProxyJavaWithDynamicPointcutAndProxyTargetClass() { } @Test - void testManualProxyJavaWithStaticPointcutAndTwoClassLoaders() throws Exception { + void manualProxyJavaWithStaticPointcutAndTwoClassLoaders() throws Exception { LogUserAdvice logAdvice = new LogUserAdvice(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); 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 0a44be6add73..79b458d36dd9 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,7 +97,7 @@ void andOrNotReplacement() { } @Test - void testEquals() { + void equals() { TypePatternClassFilter filter1 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); TypePatternClassFilter filter2 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); TypePatternClassFilter filter3 = new TypePatternClassFilter("org.springframework.tests.*"); @@ -107,7 +107,7 @@ void testEquals() { } @Test - void testHashCode() { + void hashCodeBehavior() { TypePatternClassFilter filter1 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); TypePatternClassFilter filter2 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); TypePatternClassFilter filter3 = new TypePatternClassFilter("org.springframework.tests.*"); @@ -117,7 +117,7 @@ void testHashCode() { } @Test - void testToString() { + void toStringOutput() { TypePatternClassFilter filter1 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); TypePatternClassFilter filter2 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java index 02d968212d53..c88562fbd533 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -203,7 +203,6 @@ void perThisAspect() throws Exception { itb.getSpouse(); assertThat(maaif.isMaterialized()).isTrue(); - assertThat(imapa.getDeclaredPointcut().getMethodMatcher().matches(TestBean.class.getMethod("getAge"), null)).isTrue(); assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(0); @@ -301,7 +300,7 @@ void bindingWithSingleArg() { void bindingWithMultipleArgsDifferentlyOrdered() { ManyValuedArgs target = new ManyValuedArgs(); ManyValuedArgs mva = createProxy(target, ManyValuedArgs.class, - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new ManyValuedArgs(), "someBean"))); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new ManyValuedArgs(), "someBean"))); String a = "a"; int b = 12; @@ -320,7 +319,7 @@ void introductionOnTargetNotImplementingInterface() { NotLockable notLockableTarget = new NotLockable(); assertThat(notLockableTarget).isNotInstanceOf(Lockable.class); NotLockable notLockable1 = createProxy(notLockableTarget, NotLockable.class, - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); assertThat(notLockable1).isInstanceOf(Lockable.class); Lockable lockable = (Lockable) notLockable1; assertThat(lockable.locked()).isFalse(); @@ -329,7 +328,7 @@ void introductionOnTargetNotImplementingInterface() { NotLockable notLockable2Target = new NotLockable(); NotLockable notLockable2 = createProxy(notLockable2Target, NotLockable.class, - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); assertThat(notLockable2).isInstanceOf(Lockable.class); Lockable lockable2 = (Lockable) notLockable2; assertThat(lockable2.locked()).isFalse(); @@ -343,20 +342,19 @@ void introductionOnTargetNotImplementingInterface() { void introductionAdvisorExcludedFromTargetImplementingInterface() { assertThat(AopUtils.findAdvisorsThatCanApply( getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeLockable(), "someBean")), + aspectInstanceFactory(new MakeLockable(), "someBean")), CannotBeUnlocked.class)).isEmpty(); assertThat(AopUtils.findAdvisorsThatCanApply(getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeLockable(),"someBean")), NotLockable.class)).hasSize(2); + aspectInstanceFactory(new MakeLockable(),"someBean")), NotLockable.class)).hasSize(2); } @Test void introductionOnTargetImplementingInterface() { CannotBeUnlocked target = new CannotBeUnlocked(); Lockable proxy = createProxy(target, CannotBeUnlocked.class, - // Ensure that we exclude AopUtils.findAdvisorsThatCanApply( - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), - CannotBeUnlocked.class)); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), + CannotBeUnlocked.class)); assertThat(proxy).isInstanceOf(Lockable.class); Lockable lockable = proxy; assertThat(lockable.locked()).as("Already locked").isTrue(); @@ -370,8 +368,8 @@ void introductionOnTargetExcludedByTypePattern() { ArrayList target = new ArrayList<>(); List proxy = createProxy(target, List.class, AopUtils.findAdvisorsThatCanApply( - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), - List.class)); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), + List.class)); assertThat(proxy).as("Type pattern must have excluded mixin").isNotInstanceOf(Lockable.class); } @@ -379,7 +377,7 @@ void introductionOnTargetExcludedByTypePattern() { void introductionBasedOnAnnotationMatch() { // gh-9980 AnnotatedTarget target = new AnnotatedTargetImpl(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeAnnotatedTypeModifiable(), "someBean")); + aspectInstanceFactory(new MakeAnnotatedTypeModifiable(), "someBean")); Object proxy = createProxy(target, AnnotatedTarget.class, advisors); assertThat(proxy).isInstanceOf(Lockable.class); Lockable lockable = (Lockable) proxy; @@ -393,9 +391,9 @@ void introductionWithArgumentBinding() { TestBean target = new TestBean(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeITestBeanModifiable(), "someBean")); + aspectInstanceFactory(new MakeITestBeanModifiable(), "someBean")); advisors.addAll(getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeLockable(), "someBean"))); + aspectInstanceFactory(new MakeLockable(), "someBean"))); Modifiable modifiable = (Modifiable) createProxy(target, ITestBean.class, advisors); assertThat(modifiable).isInstanceOf(Modifiable.class); @@ -426,25 +424,25 @@ 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); } - // TODO document this behaviour. - // Is it different AspectJ behaviour, at least for checked exceptions? + // TODO document this behavior. + // Is it different AspectJ behavior, at least for checked exceptions? @Test void aspectMethodThrowsExceptionIllegalOnSignature() { TestBean target = new TestBean(); RemoteException expectedException = new RemoteException(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); + aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); assertThat(advisors).as("One advice method was found").hasSize(1); ITestBean itb = createProxy(target, ITestBean.class, advisors); assertThatExceptionOfType(UndeclaredThrowableException.class) - .isThrownBy(itb::getAge) - .withCause(expectedException); + .isThrownBy(itb::getAge) + .withCause(expectedException); } @Test @@ -452,7 +450,7 @@ void twoAdvicesOnOneAspect() { TestBean target = new TestBean(); TwoAdviceAspect twoAdviceAspect = new TwoAdviceAspect(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(twoAdviceAspect, "someBean")); + aspectInstanceFactory(twoAdviceAspect, "someBean")); assertThat(advisors).as("Two advice methods found").hasSize(2); ITestBean itb = createProxy(target, ITestBean.class, advisors); itb.setName(""); @@ -466,7 +464,7 @@ void twoAdvicesOnOneAspect() { void afterAdviceTypes() throws Exception { InvocationTrackingAspect aspect = new InvocationTrackingAspect(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(aspect, "exceptionHandlingAspect")); + aspectInstanceFactory(aspect, "exceptionHandlingAspect")); Echo echo = createProxy(new Echo(), Echo.class, advisors); assertThat(aspect.invocations).isEmpty(); @@ -475,7 +473,7 @@ void afterAdviceTypes() throws Exception { aspect.invocations.clear(); assertThatExceptionOfType(FileNotFoundException.class) - .isThrownBy(() -> echo.echo(new FileNotFoundException())); + .isThrownBy(() -> echo.echo(new FileNotFoundException())); assertThat(aspect.invocations).containsExactly("around - start", "before", "after throwing", "after", "around - end"); } @@ -487,7 +485,6 @@ void nonAbstractParentAspect() { assertThat(Modifier.isAbstract(aspect.getClass().getSuperclass().getModifiers())).isFalse(); List advisors = getAdvisorFactory().getAdvisors(aspectInstanceFactory(aspect, "incrementingAspect")); - ITestBean proxy = createProxy(new TestBean("Jane", 42), ITestBean.class, advisors); assertThat(proxy.getAge()).isEqualTo(86); // (42 + 1) * 2 } @@ -812,19 +809,19 @@ void before() { invocations.add("before"); } - @AfterReturning("echo()") - void afterReturning() { - invocations.add("after returning"); + @After("echo()") + void after() { + invocations.add("after"); } - @AfterThrowing("echo()") - void afterThrowing() { - invocations.add("after throwing"); + @AfterReturning(pointcut = "this(target) && execution(* echo(*))", returning = "returnValue") + void afterReturning(JoinPoint joinPoint, Echo target, Object returnValue) { + invocations.add("after returning"); } - @After("echo()") - void after() { - invocations.add("after"); + @AfterThrowing(pointcut = "this(target) && execution(* echo(*))", throwing = "exception") + void afterThrowing(JoinPoint joinPoint, Echo target, Throwable exception) { + invocations.add("after throwing"); } } @@ -967,7 +964,7 @@ private Method getGetterFromSetter(Method setter) { class MakeITestBeanModifiable extends AbstractMakeModifiable { @DeclareParents(value = "org.springframework.beans.testfixture.beans.ITestBean+", - defaultImpl=ModifiableImpl.class) + defaultImpl = ModifiableImpl.class) static MutableModifiable mixin; } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java index 8381a2ba16ea..dbc7ce680b7b 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java index fedf491617b5..5ac1e2ee000e 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.aop.aspectj.annotation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GenerationContext; @@ -26,7 +27,6 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -36,6 +36,7 @@ * Tests for {@link AspectJAdvisorBeanRegistrationAotProcessor}. * * @author Sebastien Deleuze + * @since 6.1 */ class AspectJAdvisorBeanRegistrationAotProcessorTests { @@ -43,10 +44,11 @@ class AspectJAdvisorBeanRegistrationAotProcessorTests { private final RuntimeHints runtimeHints = this.generationContext.getRuntimeHints(); + @Test - void shouldProcessesAspectJClass() { + void shouldProcessAspectJClass() { process(AspectJClass.class); - assertThat(reflection().onType(AspectJClass.class).withMemberCategory(MemberCategory.DECLARED_FIELDS)) + assertThat(reflection().onType(AspectJClass.class).withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)) .accepts(this.runtimeHints); } @@ -63,8 +65,7 @@ void process(Class beanClass) { } } - @Nullable - private static BeanRegistrationAotContribution createContribution(Class beanClass) { + private static @Nullable BeanRegistrationAotContribution createContribution(Class beanClass) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); return new AspectJAdvisorBeanRegistrationAotProcessor() diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessorTests.java index 810409ee0fda..4db2e05de185 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-present the original author 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 79a67904814b..139db42deda7 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 AspectJPointcutAdvisorTests { @Test - void testSingleton() throws SecurityException, NoSuchMethodException { + void singleton() throws SecurityException, NoSuchMethodException { AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(CommonExpressions.MATCH_ALL_METHODS); @@ -53,7 +53,7 @@ void testSingleton() throws SecurityException, NoSuchMethodException { } @Test - void testPerTarget() throws SecurityException, NoSuchMethodException { + void perTarget() throws SecurityException, NoSuchMethodException { AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); ajexp.setExpression(CommonExpressions.MATCH_ALL_METHODS); @@ -63,8 +63,7 @@ void testPerTarget() throws SecurityException, NoSuchMethodException { 1, "someBean"); assertThat(ajpa.getAspectMetadata().getPerClausePointcut()).isNotSameAs(Pointcut.TRUE); - boolean condition = ajpa.getAspectMetadata().getPerClausePointcut() instanceof AspectJExpressionPointcut; - assertThat(condition).isTrue(); + assertThat(ajpa.getAspectMetadata().getPerClausePointcut()).isInstanceOf(AspectJExpressionPointcut.class); assertThat(ajpa.isPerInstance()).isTrue(); assertThat(ajpa.getAspectMetadata().getPerClausePointcut().getClassFilter().matches(TestBean.class)).isTrue(); @@ -76,13 +75,13 @@ void testPerTarget() throws SecurityException, NoSuchMethodException { } @Test - void testPerCflowTarget() { + void perCflowTarget() { assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> testIllegalInstantiationModel(AbstractAspectJAdvisorFactoryTests.PerCflowAspect.class)); } @Test - void testPerCflowBelowTarget() { + void perCflowBelowTarget() { assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> testIllegalInstantiationModel(AbstractAspectJAdvisorFactoryTests.PerCflowBelowAspect.class)); } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java index 242d72ce9934..297c5b2d9341 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 9e45538c713f..4abce082d099 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,13 +39,13 @@ class AspectProxyFactoryTests { @Test - void testWithNonAspect() { + void withNonAspect() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); assertThatIllegalArgumentException().isThrownBy(() -> proxyFactory.addAspect(TestBean.class)); } @Test - void testWithSimpleAspect() { + void withSimpleAspect() { TestBean bean = new TestBean(); bean.setAge(2); AspectJProxyFactory proxyFactory = new AspectJProxyFactory(bean); @@ -55,7 +55,7 @@ void testWithSimpleAspect() { } @Test - void testWithPerThisAspect() { + void withPerThisAspect() { TestBean bean1 = new TestBean(); TestBean bean2 = new TestBean(); @@ -75,14 +75,14 @@ void testWithPerThisAspect() { } @Test - void testWithInstanceWithNonAspect() { + void withInstanceWithNonAspect() { AspectJProxyFactory pf = new AspectJProxyFactory(); assertThatIllegalArgumentException().isThrownBy(() -> pf.addAspect(new TestBean())); } @Test @SuppressWarnings("unchecked") - void testSerializable() throws Exception { + void serializable() throws Exception { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(LoggingAspectOnVarargs.class); ITestBean proxy = proxyFactory.getProxy(); @@ -92,7 +92,7 @@ void testSerializable() throws Exception { } @Test - void testWithInstance() throws Exception { + void withInstance() throws Exception { MultiplyReturnValue aspect = new MultiplyReturnValue(); int multiple = 3; aspect.setMultiple(multiple); @@ -111,14 +111,14 @@ void testWithInstance() throws Exception { } @Test - void testWithNonSingletonAspectInstance() { + void withNonSingletonAspectInstance() { AspectJProxyFactory pf = new AspectJProxyFactory(); assertThatIllegalArgumentException().isThrownBy(() -> pf.addAspect(new PerThisAspect())); } @Test // SPR-13328 @SuppressWarnings("unchecked") - public void testProxiedVarargsWithEnumArray() { + void proxiedVarargsWithEnumArray() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(LoggingAspectOnVarargs.class); ITestBean proxy = proxyFactory.getProxy(); @@ -127,7 +127,7 @@ public void testProxiedVarargsWithEnumArray() { @Test // SPR-13328 @SuppressWarnings("unchecked") - public void testUnproxiedVarargsWithEnumArray() { + void unproxiedVarargsWithEnumArray() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(LoggingAspectOnSetter.class); ITestBean proxy = proxyFactory.getProxy(); diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactoryTests.java index cf7a821c06ff..9d0f974198a0 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 f12c6b982a43..7c273906f4d2 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ class AspectJNamespaceHandlerTests { @BeforeEach - public void setUp() { + void setUp() { SourceExtractor sourceExtractor = new PassThroughSourceExtractor(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.registry); XmlReaderContext readerContext = @@ -56,7 +56,7 @@ public void setUp() { } @Test - void testRegisterAutoProxyCreator() { + void registerAutoProxyCreator() { AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); @@ -65,7 +65,7 @@ void testRegisterAutoProxyCreator() { } @Test - void testRegisterAspectJAutoProxyCreator() { + void registerAspectJAutoProxyCreator() { AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); @@ -77,7 +77,7 @@ void testRegisterAspectJAutoProxyCreator() { } @Test - void testRegisterAspectJAutoProxyCreatorWithExistingAutoProxyCreator() { + void registerAspectJAutoProxyCreatorWithExistingAutoProxyCreator() { AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(this.parserContext, null); assertThat(registry.getBeanDefinitionCount()).isEqualTo(1); @@ -89,7 +89,7 @@ void testRegisterAspectJAutoProxyCreatorWithExistingAutoProxyCreator() { } @Test - void testRegisterAutoProxyCreatorWhenAspectJAutoProxyCreatorAlreadyExists() { + void registerAutoProxyCreatorWhenAspectJAutoProxyCreatorAlreadyExists() { 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 118828f338b4..8cc022645f74 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice; import org.springframework.aop.aspectj.AspectJPointcutAdvisor; import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -48,24 +49,21 @@ class AspectJPrecedenceComparatorTests { private static final int LATE_ADVICE_DECLARATION_ORDER = 10; - private AspectJPrecedenceComparator comparator; + private final AspectJPrecedenceComparator comparator = new AspectJPrecedenceComparator(); - private Method anyOldMethod; + private final Method anyOldMethod = ClassUtils.getMethod(MessageService.class, "getMessage"); - private AspectJExpressionPointcut anyOldPointcut; + private final AspectJExpressionPointcut anyOldPointcut = new AspectJExpressionPointcut(); @BeforeEach - public void setUp() { - this.comparator = new AspectJPrecedenceComparator(); - this.anyOldMethod = getClass().getMethods()[0]; - this.anyOldPointcut = new AspectJExpressionPointcut(); + void setUp() { this.anyOldPointcut.setExpression("execution(* *(..))"); } @Test - void testSameAspectNoAfterAdvice() { + void sameAspectNoAfterAdvice() { 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 +74,7 @@ void testSameAspectNoAfterAdvice() { } @Test - void testSameAspectAfterAdvice() { + void sameAspectAfterAdvice() { 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 +85,14 @@ void testSameAspectAfterAdvice() { } @Test - void testSameAspectOneOfEach() { + void sameAspectOneOfEach() { 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 - void testSameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { + void sameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { 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 +103,7 @@ void testSameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { } @Test - void testSameAdvisorPrecedenceDifferentAspectAfterAdvice() { + void sameAdvisorPrecedenceDifferentAspectAfterAdvice() { 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 +114,7 @@ void testSameAdvisorPrecedenceDifferentAspectAfterAdvice() { } @Test - void testHigherAdvisorPrecedenceNoAfterAdvice() { + void higherAdvisorPrecedenceNoAfterAdvice() { 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 +125,7 @@ void testHigherAdvisorPrecedenceNoAfterAdvice() { } @Test - void testHigherAdvisorPrecedenceAfterAdvice() { + void higherAdvisorPrecedenceAfterAdvice() { 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 +136,7 @@ void testHigherAdvisorPrecedenceAfterAdvice() { } @Test - void testLowerAdvisorPrecedenceNoAfterAdvice() { + void lowerAdvisorPrecedenceNoAfterAdvice() { 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 +147,7 @@ void testLowerAdvisorPrecedenceNoAfterAdvice() { } @Test - void testLowerAdvisorPrecedenceAfterAdvice() { + void lowerAdvisorPrecedenceAfterAdvice() { 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); @@ -209,4 +207,11 @@ private Advisor createSpringAOPBeforeAdvice(int order) { return advisor; } + static class MessageService { + + public String getMessage() { + return "test"; + } + } + } diff --git a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java index c6d60133c446..11dd9a436db8 100644 --- a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java index 3d1bea7b13d0..f2e538660e0d 100644 --- a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java b/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java index 7e34bc23ef32..db6bf9d9c681 100644 --- a/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..e4c57df620a9 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/AbstractProxyExceptionHandlingTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.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() { + try { + Objects.requireNonNull(proxy).doSomething(); + } + catch (Throwable throwable) { + throwableSeenByCaller = throwable; + } + } + + @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/AopProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java index 9199b005712d..a59f83def42a 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..165587ddf178 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/CglibProxyExceptionHandlingTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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/IntroductionBenchmarkTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java index 33326d426a6d..a429381dbc0e 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..9784101af2fe --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/JdkProxyExceptionHandlingTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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/MethodInvocationTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/MethodInvocationTests.java index 91091035aac7..f69e0f1589f1 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/MethodInvocationTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/MethodInvocationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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 @@ class MethodInvocationTests { @Test - void testValidInvocation() throws Throwable { + void validInvocation() throws Throwable { Method method = Object.class.getMethod("hashCode"); Object proxy = new Object(); Object returnValue = new Object(); @@ -49,7 +49,7 @@ void testValidInvocation() throws Throwable { * toString on target can cause failure. */ @Test - void testToStringDoesntHitTarget() throws Throwable { + void toStringDoesntHitTarget() throws Throwable { Object target = new TestBean() { @Override public String toString() { 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 abf36f2adda9..0cc525d3095e 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ interface Foo { } @Test - void testNullPrimitiveWithJdkProxy() { + void nullPrimitiveWithJdkProxy() { class SimpleFoo implements Foo { @Override @@ -62,7 +62,7 @@ public int getValue() { } @Test - void testNullPrimitiveWithCglibProxy() { + void nullPrimitiveWithCglibProxy() { Bar target = new Bar(); ProxyFactory factory = new ProxyFactory(target); 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 826191596593..d82a4957b6d0 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ class PrototypeTargetTests { @Test - void testPrototypeProxyWithPrototypeTarget() { + void prototypeProxyWithPrototypeTarget() { TestBeanImpl.constructionCount = 0; DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); @@ -52,7 +52,7 @@ void testPrototypeProxyWithPrototypeTarget() { } @Test - void testSingletonProxyWithPrototypeTarget() { + void singletonProxyWithPrototypeTarget() { TestBeanImpl.constructionCount = 0; DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyExceptionHandlingTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyExceptionHandlingTests.java deleted file mode 100644 index a30b4a502a5d..000000000000 --- a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyExceptionHandlingTests.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.aop.framework; - -import java.lang.reflect.Proxy; -import java.lang.reflect.UndeclaredThrowableException; -import java.util.Objects; - -import org.aopalliance.intercept.MethodInterceptor; -import org.assertj.core.api.WithAssertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.mockito.stubbing.Answer; - -import org.springframework.cglib.proxy.Enhancer; -import org.springframework.lang.Nullable; - -import static org.mockito.BDDMockito.doAnswer; -import static org.mockito.BDDMockito.doThrow; -import static org.mockito.BDDMockito.mock; - -/** - * @author Mikaël Francoeur - * @since 6.2 - */ -abstract class ProxyExceptionHandlingTests implements WithAssertions { - - 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(MyClass.class); - - protected final ProxyFactory proxyFactory = new ProxyFactory(target); - - @Nullable - protected MyInterface proxy; - - @Nullable - private Throwable throwableSeenByCaller; - - - @BeforeEach - void clear() { - Mockito.clearInvocations(target); - } - - protected void assertProxyType(Object proxy) { - } - - private void invokeProxy() { - throwableSeenByCaller = catchThrowable(() -> Objects.requireNonNull(proxy).doSomething()); - } - - @SuppressWarnings("SameParameterValue") - private Answer sneakyThrow(Throwable throwable) { - return invocation -> { - throw throwable; - }; - } - - - static class JdkAopProxyTests extends ProxyExceptionHandlingTests { - - @Override - protected void assertProxyType(Object proxy) { - assertThat(Proxy.isProxyClass(proxy.getClass())).isTrue(); - } - } - - - static class CglibAopProxyTests extends ProxyExceptionHandlingTests { - - @BeforeEach - void setup() { - proxyFactory.setProxyTargetClass(true); - } - - @Override - protected void assertProxyType(Object proxy) { - assertThat(Enhancer.isEnhanced(proxy.getClass())).isTrue(); - } - } - - - @Nested - class WhenThereIsOneInterceptor { - - @Nullable - private Throwable throwableSeenByInterceptor; - - @BeforeEach - void beforeEach() { - proxyFactory.addAdvice(captureThrowable()); - proxy = (MyInterface) proxyFactory.getProxy(ProxyExceptionHandlingTests.class.getClassLoader()); - assertProxyType(proxy); - } - - @Test - void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException { - doAnswer(sneakyThrow(undeclaredCheckedException)).when(target).doSomething(); - invokeProxy(); - assertThat(throwableSeenByInterceptor).isSameAs(undeclaredCheckedException); - assertThat(throwableSeenByCaller) - .isInstanceOf(UndeclaredThrowableException.class) - .hasCauseReference(undeclaredCheckedException); - } - - @Test - void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException { - doThrow(declaredCheckedException).when(target).doSomething(); - invokeProxy(); - assertThat(throwableSeenByInterceptor).isSameAs(declaredCheckedException); - assertThat(throwableSeenByCaller).isSameAs(declaredCheckedException); - } - - @Test - void targetThrowsUncheckedException() throws DeclaredCheckedException { - doThrow(uncheckedException).when(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 - class WhenThereAreNoInterceptors { - - @BeforeEach - void beforeEach() { - proxy = (MyInterface) proxyFactory.getProxy(ProxyExceptionHandlingTests.class.getClassLoader()); - assertProxyType(proxy); - } - - @Test - void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException { - doAnswer(sneakyThrow(undeclaredCheckedException)).when(target).doSomething(); - invokeProxy(); - assertThat(throwableSeenByCaller) - .isInstanceOf(UndeclaredThrowableException.class) - .hasCauseReference(undeclaredCheckedException); - } - - @Test - void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException { - doThrow(declaredCheckedException).when(target).doSomething(); - invokeProxy(); - assertThat(throwableSeenByCaller).isSameAs(declaredCheckedException); - } - - @Test - void targetThrowsUncheckedException() throws DeclaredCheckedException { - doThrow(uncheckedException).when(target).doSomething(); - invokeProxy(); - assertThat(throwableSeenByCaller).isSameAs(uncheckedException); - } - } - - - protected interface MyInterface { - - void doSomething() throws DeclaredCheckedException; - } - - static class MyClass implements MyInterface { - - @Override - public void doSomething() throws DeclaredCheckedException { - throw declaredCheckedException; - } - } - - @SuppressWarnings("serial") - protected static class UndeclaredCheckedException extends Exception { - } - - @SuppressWarnings("serial") - protected static class DeclaredCheckedException extends Exception { - } - -} 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 1bda9760714a..d8e4ff4ebe5e 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -189,25 +189,22 @@ 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(); @@ -224,26 +221,26 @@ public Object invoke(MethodInvocation invocation) { 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 void sealedInterfaceExclusion() { // String implements ConstantDesc on JDK 12+, sealed as of JDK 17 - ProxyFactory factory = new ProxyFactory(""); + 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); } @@ -330,6 +327,45 @@ void proxyTargetClassWithConcreteClassAsTarget() { assertThat(AopProxyUtils.ultimateTargetClass(proxy2)).isEqualTo(TestBean.class); } + @Test + 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 void interfaceProxiesCanBeOrderedThroughAnnotations() { Object proxy1 = new ProxyFactory(new A()).getProxy(); 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..2bd1aca08815 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/SentenceFragment.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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..81861dbc9bd4 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/SentenceFragmentDisplayNameGenerator.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 1a669c018e0d..3908caf8ac5e 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,14 +40,14 @@ class ThrowsAdviceInterceptorTests { @Test - void testNoHandlerMethods() { + void noHandlerMethods() { // should require one handler method at least assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> new ThrowsAdviceInterceptor(new Object())); } @Test - void testNotInvoked() throws Throwable { + void notInvoked() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); Object ret = new Object(); @@ -58,7 +58,7 @@ void testNotInvoked() throws Throwable { } @Test - void testNoHandlerMethodForThrowable() throws Throwable { + void noHandlerMethodForThrowable() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); assertThat(ti.getHandlerMethodCount()).isEqualTo(2); @@ -70,7 +70,7 @@ void testNoHandlerMethodForThrowable() throws Throwable { } @Test - void testCorrectHandlerUsed() throws Throwable { + void correctHandlerUsed() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); FileNotFoundException ex = new FileNotFoundException(); @@ -84,7 +84,7 @@ void testCorrectHandlerUsed() throws Throwable { } @Test - void testCorrectHandlerUsedForSubclass() throws Throwable { + void correctHandlerUsedForSubclass() throws Throwable { MyThrowsHandler th = new MyThrowsHandler(); ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); // Extends RemoteException @@ -97,7 +97,7 @@ void testCorrectHandlerUsedForSubclass() throws Throwable { } @Test - void testHandlerMethodThrowsException() throws Throwable { + void handlerMethodThrowsException() throws Throwable { final Throwable t = new Throwable(); MyThrowsHandler th = new MyThrowsHandler() { 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..ec751f46e33c --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 + */ +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 616c855f3e32..82d5fa7ba525 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.ProxyFactory; @@ -30,21 +32,23 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link ConcurrencyThrottleInterceptor}. + * * @author Juergen Hoeller * @author Chris Beams * @since 06.04.2004 */ class ConcurrencyThrottleInterceptorTests { - protected static final Log logger = LogFactory.getLog(ConcurrencyThrottleInterceptorTests.class); + private static final Log logger = LogFactory.getLog(ConcurrencyThrottleInterceptorTests.class); - public static final int NR_OF_THREADS = 100; + private static final int NR_OF_THREADS = 100; - public static final int NR_OF_ITERATIONS = 1000; + private static final int NR_OF_ITERATIONS = 1000; @Test - void testSerializable() throws Exception { + void interceptorMustBeSerializable() throws Exception { DerivedTestBean tb = new DerivedTestBean(); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setInterfaces(ITestBean.class); @@ -62,17 +66,9 @@ void testSerializable() throws Exception { serializedProxy.getAge(); } - @Test - void testMultipleThreadsWithLimit1() { - testMultipleThreads(1); - } - - @Test - void testMultipleThreadsWithLimit10() { - testMultipleThreads(10); - } - - private void testMultipleThreads(int concurrencyLimit) { + @ParameterizedTest + @ValueSource(ints = {1, 10}) + void multipleThreadsWithLimit(int concurrencyLimit) { TestBean tb = new TestBean(); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setInterfaces(ITestBean.class); 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 44523196bbbf..48cff3f28e40 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java index 0c462a7c86a2..32949c7bec86 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ class DebugInterceptorTests { @Test - void testSunnyDayPathLogsCorrectly() throws Throwable { + void sunnyDayPathLogsCorrectly() throws Throwable { MethodInvocation methodInvocation = mock(); Log log = mock(); @@ -52,7 +52,7 @@ void testSunnyDayPathLogsCorrectly() throws Throwable { } @Test - void testExceptionPathStillLogsCorrectly() throws Throwable { + void exceptionPathStillLogsCorrectly() 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 565e3e005a42..564aa737d073 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ public int getAge() { } @Test - void testNoIntroduction() { + void noIntroduction() { String beanName = "foo"; TestBean target = new RequiresBeanNameBoundTestBean(beanName); ProxyFactory pf = new ProxyFactory(target); @@ -54,14 +54,13 @@ void testNoIntroduction() { pf.addAdvisor(ExposeBeanNameAdvisors.createAdvisorWithoutIntroduction(beanName)); ITestBean proxy = (ITestBean) pf.getProxy(); - boolean condition = proxy instanceof NamedBean; - assertThat(condition).as("No introduction").isFalse(); + assertThat(proxy).as("No introduction").isNotInstanceOf(NamedBean.class); // Requires binding proxy.getAge(); } @Test - void testWithIntroduction() { + void withIntroduction() { String beanName = "foo"; TestBean target = new RequiresBeanNameBoundTestBean(beanName); ProxyFactory pf = new ProxyFactory(target); @@ -69,8 +68,7 @@ void testWithIntroduction() { pf.addAdvisor(ExposeBeanNameAdvisors.createAdvisorIntroducingNamedBean(beanName)); ITestBean proxy = (ITestBean) pf.getProxy(); - boolean condition = proxy instanceof NamedBean; - assertThat(condition).as("Introduction was made").isTrue(); + assertThat(proxy).as("Introduction was made").isInstanceOf(NamedBean.class); // Requires binding proxy.getAge(); 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 79726a94b4d0..c745d5eefeba 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class ExposeInvocationInterceptorTests { @Test - void testXmlConfig() { + void xmlConfig() { 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/ExposedInvocationTestBean.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposedInvocationTestBean.java index 95e8b7d2d39a..f4f5ca3c1e22 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposedInvocationTestBean.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposedInvocationTestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/InvocationCheckExposedInvocationTestBean.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/InvocationCheckExposedInvocationTestBean.java index 281252732170..9aa862a45408 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/InvocationCheckExposedInvocationTestBean.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/InvocationCheckExposedInvocationTestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java index 6cc67b4da573..adce49ea081e 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class PerformanceMonitorInterceptorTests { @Test - void testSuffixAndPrefixAssignment() { + void suffixAndPrefixAssignment() { PerformanceMonitorInterceptor interceptor = new PerformanceMonitorInterceptor(); assertThat(interceptor.getPrefix()).isNotNull(); @@ -49,7 +49,7 @@ void testSuffixAndPrefixAssignment() { } @Test - void testSunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { + void sunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { MethodInvocation mi = mock(); given(mi.getMethod()).willReturn(String.class.getMethod("toString")); @@ -62,7 +62,7 @@ void testSunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { } @Test - void testExceptionPathStillLogsPerformanceMetricsCorrectly() throws Throwable { + void exceptionPathStillLogsPerformanceMetricsCorrectly() throws Throwable { MethodInvocation mi = mock(); given(mi.getMethod()).willReturn(String.class.getMethod("toString")); 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 b977f97a4c00..e95b8b8db2f7 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class SimpleTraceInterceptorTests { @Test - void testSunnyDayPathLogsCorrectly() throws Throwable { + void sunnyDayPathLogsCorrectly() throws Throwable { MethodInvocation mi = mock(); given(mi.getMethod()).willReturn(String.class.getMethod("toString")); given(mi.getThis()).willReturn(this); @@ -51,7 +51,7 @@ void testSunnyDayPathLogsCorrectly() throws Throwable { } @Test - void testExceptionPathStillLogsCorrectly() throws Throwable { + void exceptionPathStillLogsCorrectly() 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 ee21418518b5..7b5ee97bcc00 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,25 +35,25 @@ class DefaultScopedObjectTests { @Test - void testCtorWithNullBeanFactory() { + void ctorWithNullBeanFactory() { assertThatIllegalArgumentException().isThrownBy(() -> new DefaultScopedObject(null, GOOD_BEAN_NAME)); } @Test - void testCtorWithNullTargetBeanName() { + void ctorWithNullTargetBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> testBadTargetBeanName(null)); } @Test - void testCtorWithEmptyTargetBeanName() { + void ctorWithEmptyTargetBeanName() { assertThatIllegalArgumentException().isThrownBy(() -> testBadTargetBeanName("")); } @Test - void testCtorWithJustWhitespacedTargetBeanName() { + void ctorWithJustWhitespacedTargetBeanName() { 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 0a8b727a9435..66f6670c90f2 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class ScopedProxyAutowireTests { @Test - void testScopedProxyInheritsAutowireCandidateFalse() { + void scopedProxyInheritsAutowireCandidateFalse() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(ScopedProxyAutowireTests.class, "scopedAutowireFalse.xml")); @@ -48,7 +48,7 @@ void testScopedProxyInheritsAutowireCandidateFalse() { } @Test - void testScopedProxyReplacesAutowireCandidateTrue() { + void scopedProxyReplacesAutowireCandidateTrue() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( qualifiedResource(ScopedProxyAutowireTests.class, "scopedAutowireTrue.xml")); diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessorTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessorTests.java index 778ac1dd7b06..702cef497ed7 100644 --- a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java index ff28009a7fb6..de5e76db2b71 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,15 @@ import org.junit.jupiter.api.Test; +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.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -25,6 +34,7 @@ * Tests for {@link ScopedProxyUtils}. * * @author Sam Brannen + * @author Juergen Hoeller * @since 5.1.10 */ class ScopedProxyUtilsTests { @@ -53,15 +63,79 @@ void getOriginalBeanNameAndIsScopedTarget() { @Test void getOriginalBeanNameForNullTargetBean() { assertThatIllegalArgumentException() - .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName(null)) - .withMessage("bean name 'null' does not refer to the target of a scoped proxy"); + .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName(null)) + .withMessage("bean name 'null' does not refer to the target of a scoped proxy"); } @Test void getOriginalBeanNameForNonScopedTarget() { assertThatIllegalArgumentException() - .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName("myBean")) - .withMessage("bean name 'myBean' does not refer to the target of a scoped proxy"); + .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName("myBean")) + .withMessage("bean name 'myBean' does not refer to the target of a scoped proxy"); + } + + @Test + void createScopedProxyTargetAppliesAutowireSettingsToProxyBeanDefinition() { + AbstractBeanDefinition targetDefinition = new GenericBeanDefinition(); + // Opposite of defaults + targetDefinition.setAutowireCandidate(false); + targetDefinition.setDefaultCandidate(false); + targetDefinition.setPrimary(true); + targetDefinition.setFallback(true); + + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy( + new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false); + AbstractBeanDefinition proxyBeanDefinition = (AbstractBeanDefinition) proxyHolder.getBeanDefinition(); + + assertThat(proxyBeanDefinition.isAutowireCandidate()).isFalse(); + assertThat(proxyBeanDefinition.isDefaultCandidate()).isFalse(); + assertThat(proxyBeanDefinition.isPrimary()).isTrue(); + assertThat(proxyBeanDefinition.isFallback()).isTrue(); + } + + @Test + void createScopedProxyTargetAppliesBeanAttributesToProxyBeanDefinition() { + GenericBeanDefinition targetDefinition = new GenericBeanDefinition(); + // Opposite of defaults + targetDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + targetDefinition.setSource("theSource"); + targetDefinition.addQualifier(new AutowireCandidateQualifier("myQualifier")); + + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy( + new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false); + BeanDefinition proxyBeanDefinition = proxyHolder.getBeanDefinition(); + + assertThat(proxyBeanDefinition.getRole()).isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + assertThat(proxyBeanDefinition).isInstanceOf(RootBeanDefinition.class); + assertThat(proxyBeanDefinition.getPropertyValues()).hasSize(2); + assertThat(proxyBeanDefinition.getPropertyValues().get("proxyTargetClass")).isEqualTo(false); + assertThat(proxyBeanDefinition.getPropertyValues().get("targetBeanName")).isEqualTo( + ScopedProxyUtils.getTargetBeanName("myBean")); + + RootBeanDefinition rootBeanDefinition = (RootBeanDefinition) proxyBeanDefinition; + assertThat(rootBeanDefinition.getQualifiers()).hasSize(1); + assertThat(rootBeanDefinition.hasQualifier("myQualifier")).isTrue(); + assertThat(rootBeanDefinition.getSource()).isEqualTo("theSource"); + } + + @Test + void createScopedProxyTargetCleansAutowireSettingsInTargetDefinition() { + AbstractBeanDefinition targetDefinition = new GenericBeanDefinition(); + targetDefinition.setAutowireCandidate(true); + targetDefinition.setDefaultCandidate(true); + targetDefinition.setPrimary(true); + targetDefinition.setFallback(true); + + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + ScopedProxyUtils.createScopedProxy( + new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false); + + assertThat(targetDefinition.isAutowireCandidate()).isFalse(); + assertThat(targetDefinition.isDefaultCandidate()).isFalse(); + assertThat(targetDefinition.isPrimary()).isFalse(); + assertThat(targetDefinition.isFallback()).isFalse(); } } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java index fb4d1ed6f717..e6c55102d6c5 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.lang.reflect.Method; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.ClassFilter; @@ -31,7 +32,6 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.ResolvableType; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -45,7 +45,7 @@ class AopUtilsTests { @Test - void testPointcutCanNeverApply() { + void pointcutCanNeverApply() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazzy) { @@ -58,13 +58,13 @@ public boolean matches(Method method, @Nullable Class clazzy) { } @Test - void testPointcutAlwaysApplies() { + void pointcutAlwaysApplies() { assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), Object.class)).isTrue(); assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), TestBean.class)).isTrue(); } @Test - void testPointcutAppliesToOneMethodOnObject() { + void pointcutAppliesToOneMethodOnObject() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazz) { @@ -84,7 +84,7 @@ public boolean matches(Method method, @Nullable Class clazz) { * that's subverted the singleton construction limitation. */ @Test - void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { + void canonicalFrameworkClassesStillCanonicalOnDeserialization() 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); @@ -95,7 +95,7 @@ void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Excep } @Test - void testInvokeJoinpointUsingReflection() throws Throwable { + void invokeJoinpointUsingReflection() throws Throwable { String name = "foo"; TestBean testBean = new TestBean(name); Method method = ReflectionUtils.findMethod(TestBean.class, "getName"); 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 f85b93bfcb82..f87d103f9a20 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java index f1fffbdf9cb4..71c40c53619e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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 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 54b3657703f1..e2a58d829095 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; @@ -58,14 +58,14 @@ public boolean matches(Method m, @Nullable Class targetClass) { @Test - void testMatchAll() throws NoSuchMethodException { + void matchAll() 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 - void testFilterByClass() { + void filterByClass() { ComposablePointcut pc = new ComposablePointcut(); assertThat(pc.getClassFilter().matches(Object.class)).isTrue(); @@ -85,7 +85,7 @@ void testFilterByClass() { } @Test - void testUnionMethodMatcher() { + void unionMethodMatcher() { // 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(); @@ -108,7 +108,7 @@ void testUnionMethodMatcher() { } @Test - void testIntersectionMethodMatcher() { + void intersectionMethodMatcher() { 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(); @@ -125,7 +125,7 @@ void testIntersectionMethodMatcher() { } @Test - void testEqualsAndHashCode() { + void equalsAndHashCode() { ComposablePointcut pc1 = new ComposablePointcut(); ComposablePointcut pc2 = new ComposablePointcut(); 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 65c08b8725f5..30a05cd4288b 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-2024 the original author or authors. + * Copyright 2002-present the original author 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. @@ -152,7 +152,7 @@ void equalsAndHashCode() { } @Test - void testToString() { + void toStringOutput() { String pointcutType = ControlFlowPointcut.class.getName(); String componentType = MyComponent.class.getName(); 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 de5a55463999..b05a68ae801f 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-2024 the original author or authors. + * Copyright 2002-present the original author 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() { + void nullTarget() { // Shouldn't accept null target assertThatIllegalArgumentException().isThrownBy(() -> new DelegatingIntroductionInterceptor(null)); } @Test - void testIntroductionInterceptorWithDelegation() { + void introductionInterceptorWithDelegation() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(TimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -70,7 +70,7 @@ void testIntroductionInterceptorWithDelegation() { } @Test - void testIntroductionInterceptorWithInterfaceHierarchy() { + void introductionInterceptorWithInterfaceHierarchy() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(SubTimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -86,7 +86,7 @@ void testIntroductionInterceptorWithInterfaceHierarchy() { } @Test - void testIntroductionInterceptorWithSuperInterface() { + void introductionInterceptorWithSuperInterface() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(TimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -103,7 +103,7 @@ void testIntroductionInterceptorWithSuperInterface() { } @Test - void testAutomaticInterfaceRecognitionInDelegate() throws Exception { + void automaticInterfaceRecognitionInDelegate() throws Exception { final long t = 1001L; class Tester implements TimeStamped, ITester { @Override @@ -133,7 +133,7 @@ public long getTimeStamp() { @Test - void testAutomaticInterfaceRecognitionInSubclass() throws Exception { + void automaticInterfaceRecognitionInSubclass() throws Exception { final long t = 1001L; @SuppressWarnings("serial") class TestII extends DelegatingIntroductionInterceptor implements TimeStamped, ITester { @@ -178,7 +178,7 @@ public long getTimeStamp() { } @Test - void testIntroductionInterceptorDoesNotReplaceToString() { + void introductionInterceptorDoesNotReplaceToString() { TestBean raw = new TestBean(); assertThat(raw).isNotInstanceOf(TimeStamped.class); ProxyFactory factory = new ProxyFactory(raw); @@ -199,7 +199,7 @@ public String toString() { } @Test - void testDelegateReturnsThisIsMassagedToReturnProxy() { + void delegateReturnsThisIsMassagedToReturnProxy() { NestedTestBean target = new NestedTestBean(); String company = "Interface21"; target.setCompany(company); @@ -220,7 +220,7 @@ public ITestBean getSpouse() { } @Test - void testSerializableDelegatingIntroductionInterceptorSerializable() throws Exception { + void serializableDelegatingIntroductionInterceptorSerializable() throws Exception { SerializablePerson serializableTarget = new SerializablePerson(); String name = "Tony"; serializableTarget.setName("Tony"); @@ -245,7 +245,7 @@ void testSerializableDelegatingIntroductionInterceptorSerializable() throws Exce // Test when target implements the interface: should get interceptor by preference. @Test - void testIntroductionMasksTargetImplementation() { + void introductionMasksTargetImplementation() { final long t = 1001L; @SuppressWarnings("serial") class TestII extends DelegatingIntroductionInterceptor implements TimeStamped { diff --git a/spring-aop/src/test/java/org/springframework/aop/support/JdkRegexpMethodPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/JdkRegexpMethodPointcutTests.java index 055dad7a8f6a..b0efa7ccda23 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/JdkRegexpMethodPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/JdkRegexpMethodPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java b/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java index 6fae987c9ea7..e37e932fb13a 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; @@ -56,19 +56,19 @@ public MethodMatchersTests() throws Exception { @Test - void testDefaultMatchesAll() { + void defaultMatchesAll() { MethodMatcher defaultMm = MethodMatcher.TRUE; assertThat(defaultMm.matches(EXCEPTION_GETMESSAGE, Exception.class)).isTrue(); assertThat(defaultMm.matches(ITESTBEAN_SETAGE, TestBean.class)).isTrue(); } @Test - void testMethodMatcherTrueSerializable() throws Exception { + void methodMatcherTrueSerializable() throws Exception { assertThat(MethodMatcher.TRUE).isSameAs(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)); } @Test - void testSingle() { + void single() { MethodMatcher defaultMm = MethodMatcher.TRUE; assertThat(defaultMm.matches(EXCEPTION_GETMESSAGE, Exception.class)).isTrue(); assertThat(defaultMm.matches(ITESTBEAN_SETAGE, TestBean.class)).isTrue(); @@ -80,7 +80,7 @@ void testSingle() { @Test - void testDynamicAndStaticMethodMatcherIntersection() { + void dynamicAndStaticMethodMatcherIntersection() { MethodMatcher mm1 = MethodMatcher.TRUE; MethodMatcher mm2 = new TestDynamicMethodMatcherWhichMatches(); MethodMatcher intersection = MethodMatchers.intersection(mm1, mm2); @@ -95,7 +95,7 @@ void testDynamicAndStaticMethodMatcherIntersection() { } @Test - void testStaticMethodMatcherUnion() { + void staticMethodMatcherUnion() { MethodMatcher getterMatcher = new StartsWithMatcher("get"); MethodMatcher setterMatcher = new StartsWithMatcher("set"); MethodMatcher union = MethodMatchers.union(getterMatcher, setterMatcher); @@ -107,7 +107,7 @@ void testStaticMethodMatcherUnion() { } @Test - void testUnionEquals() { + void unionEquals() { MethodMatcher first = MethodMatchers.union(MethodMatcher.TRUE, MethodMatcher.TRUE); MethodMatcher second = new ComposablePointcut(MethodMatcher.TRUE).union(new ComposablePointcut(MethodMatcher.TRUE)).getMethodMatcher(); assertThat(first).isEqualTo(second); diff --git a/spring-aop/src/test/java/org/springframework/aop/support/NameMatchMethodPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/NameMatchMethodPointcutTests.java index de0344a9f0fb..6b8bc26e1eb4 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/NameMatchMethodPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/NameMatchMethodPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java index 6f50a9aecc17..3049c38310d1 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; @@ -120,7 +120,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { @Test - void testTrue() { + void trueCase() { 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 @@ void testTrue() { } @Test - void testMatches() { + void matches() { 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 @@ void testMatches() { * Should match all setters and getters on any class */ @Test - void testUnionOfSettersAndGetters() { + void unionOfSettersAndGetters() { 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 @@ void testUnionOfSettersAndGetters() { } @Test - void testUnionOfSpecificGetters() { + void unionOfSpecificGetters() { 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 @@ void testUnionOfSpecificGetters() { * Second one matches all getters in the MyTestBean class. TestBean getters shouldn't pass. */ @Test - void testUnionOfAllSettersAndSubclassSetters() { + void unionOfAllSettersAndSubclassSetters() { 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 @@ void testUnionOfAllSettersAndSubclassSetters() { * it's the union of allClassGetAge and subclass getters */ @Test - void testIntersectionOfSpecificGettersAndSubclassGetters() { + void intersectionOfSpecificGettersAndSubclassGetters() { 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 @@ void testIntersectionOfSpecificGettersAndSubclassGetters() { * The intersection of these two pointcuts leaves nothing. */ @Test - void testSimpleIntersection() { + void simpleIntersection() { 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 df0213da7423..9c69cbd1c0e3 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class RegexpMethodPointcutAdvisorIntegrationTests { @Test - void testSinglePattern() throws Throwable { + void singlePattern() throws Throwable { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); ITestBean advised = (ITestBean) bf.getBean("settersAdvised"); @@ -62,7 +62,7 @@ void testSinglePattern() throws Throwable { } @Test - void testMultiplePatterns() throws Throwable { + void multiplePatterns() 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 @@ void testMultiplePatterns() throws Throwable { } @Test - void testSerialization() throws Throwable { + void serialization() 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 ad60e60ac646..19ca266e39ea 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,19 +44,19 @@ void matches() { } @Test - void testEquals() { + void equals() { assertThat(filter1).isEqualTo(filter2); assertThat(filter1).isNotEqualTo(filter3); } @Test - void testHashCode() { + void hashCodeBehavior() { assertThat(filter1.hashCode()).isEqualTo(filter2.hashCode()); assertThat(filter1.hashCode()).isNotEqualTo(filter3.hashCode()); } @Test - void testToString() { + void toStringOutput() { assertThat(filter1.toString()).isEqualTo("org.springframework.aop.support.RootClassFilter: java.lang.Exception"); assertThat(filter1.toString()).isEqualTo(filter2.toString()); } 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 1598de413721..cffa596d5dee 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java b/spring-aop/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceProxyTests.java index 11e8adb2da4f..572003c06621 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ class CommonsPool2TargetSourceProxyTests { qualifiedResource(CommonsPool2TargetSourceProxyTests.class, "context.xml"); @Test - void testProxy() { + void proxy() { 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 9676b676a094..bd716b355b8c 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ class HotSwappableTargetSourceTests { @BeforeEach - public void setup() { + void setup() { this.beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( qualifiedResource(HotSwappableTargetSourceTests.class, "context.xml")); @@ -58,7 +58,7 @@ public void setup() { * We must simulate container shutdown, which should clear threads. */ @AfterEach - public void close() { + void close() { // Will call pool.close() this.beanFactory.destroySingletons(); } @@ -68,7 +68,7 @@ public void close() { * Check it works like a normal invoker */ @Test - void testBasicFunctionality() { + void basicFunctionality() { SideEffectBean proxied = (SideEffectBean) beanFactory.getBean("swappable"); assertThat(proxied.getCount()).isEqualTo(INITIAL_COUNT); proxied.doWork(); @@ -80,7 +80,7 @@ void testBasicFunctionality() { } @Test - void testValidSwaps() { + void validSwaps() { SideEffectBean target1 = (SideEffectBean) beanFactory.getBean("target1"); SideEffectBean target2 = (SideEffectBean) beanFactory.getBean("target2"); @@ -107,17 +107,17 @@ void testValidSwaps() { } @Test - void testRejectsSwapToNull() { + void rejectsSwapToNull() { HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper"); assertThatIllegalArgumentException().as("Shouldn't be able to swap to invalid value").isThrownBy(() -> swapper.swap(null)) .withMessageContaining("null"); // It shouldn't be corrupted, it should still work - testBasicFunctionality(); + basicFunctionality(); } @Test - void testSerialization() throws Exception { + void serialization() 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 266cfedf5093..b19ca043ce16 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class LazyCreationTargetSourceTests { @Test - void testCreateLazy() { + void createLazy() { 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 84be7d1a5323..ebd3f8d3b9ce 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-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java b/spring-aop/src/test/java/org/springframework/aop/target/PrototypeBasedTargetSourceTests.java index 6846f70962ce..abba3ea1c09b 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 PrototypeBasedTargetSourceTests { @Test - void testSerializability() throws Exception { + void serializability() throws Exception { MutablePropertyValues tsPvs = new MutablePropertyValues(); tsPvs.add("targetBeanName", "person"); RootBeanDefinition tsBd = new RootBeanDefinition(TestTargetSource.class); 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 ba12b5f6beef..e66858adf9a3 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 PrototypeTargetSourceTests { @BeforeEach - public void setup() { + void setup() { this.beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( qualifiedResource(PrototypeTargetSourceTests.class, "context.xml")); @@ -52,7 +52,7 @@ public void setup() { * With the singleton, there will be change. */ @Test - void testPrototypeAndSingletonBehaveDifferently() { + void prototypeAndSingletonBehaveDifferently() { 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 0c227ecd4be4..2d93cfaf0a7b 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class ThreadLocalTargetSourceTests { @BeforeEach - public void setup() { + void setup() { this.beanFactory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( qualifiedResource(ThreadLocalTargetSourceTests.class, "context.xml")); @@ -60,7 +60,7 @@ protected void close() { * with one another. */ @Test - void testUseDifferentManagedInstancesInSameThread() { + void useDifferentManagedInstancesInSameThread() { SideEffectBean apartment = (SideEffectBean) beanFactory.getBean("apartment"); assertThat(apartment.getCount()).isEqualTo(INITIAL_COUNT); apartment.doWork(); @@ -72,7 +72,7 @@ void testUseDifferentManagedInstancesInSameThread() { } @Test - void testReuseInSameThread() { + void reuseInSameThread() { SideEffectBean apartment = (SideEffectBean) beanFactory.getBean("apartment"); assertThat(apartment.getCount()).isEqualTo(INITIAL_COUNT); apartment.doWork(); @@ -86,7 +86,7 @@ void testReuseInSameThread() { * Relies on introduction. */ @Test - void testCanGetStatsViaMixin() { + void canGetStatsViaMixin() { ThreadLocalTargetSourceStats stats = (ThreadLocalTargetSourceStats) beanFactory.getBean("apartment"); // +1 because creating target for stats call counts assertThat(stats.getInvocationCount()).isEqualTo(1); @@ -104,7 +104,7 @@ void testCanGetStatsViaMixin() { } @Test - void testNewThreadHasOwnInstance() throws InterruptedException { + void newThreadHasOwnInstance() 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 - void testReuseDestroyedTarget() { + void reuseDestroyedTarget() { 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 3c81244348d7..31f60744387f 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class RefreshableTargetSourceTests { * Test what happens when checking for refresh but not refreshing object. */ @Test - void testRefreshCheckWithNonRefresh() throws Exception { + void refreshCheckWithNonRefresh() throws Exception { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(); ts.setRefreshCheckDelay(0); @@ -49,7 +49,7 @@ void testRefreshCheckWithNonRefresh() throws Exception { * Test what happens when checking for refresh and refresh occurs. */ @Test - void testRefreshCheckWithRefresh() throws Exception { + void refreshCheckWithRefresh() throws Exception { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(true); ts.setRefreshCheckDelay(0); @@ -65,7 +65,7 @@ void testRefreshCheckWithRefresh() throws Exception { * Test what happens when no refresh occurs. */ @Test - void testWithNoRefreshCheck() { + void withNoRefreshCheck() { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(true); ts.setRefreshCheckDelay(-1); @@ -78,7 +78,7 @@ void testWithNoRefreshCheck() { @Test @EnabledForTestGroups(LONG_RUNNING) - public void testRefreshOverTime() throws Exception { + void refreshOverTime() throws Exception { CountingRefreshableTargetSource ts = new CountingRefreshableTargetSource(true); ts.setRefreshCheckDelay(100); @@ -95,7 +95,7 @@ public void testRefreshOverTime() throws Exception { Object d = ts.getTarget(); assertThat(d).as("D should not be null").isNotNull(); - assertThat(a.equals(d)).as("A and D should not be equal").isFalse(); + assertThat(a).as("A and D should not be equal").isNotEqualTo(d); Object e = ts.getTarget(); assertThat(e).as("D and E should be equal").isEqualTo(d); @@ -103,7 +103,7 @@ public void testRefreshOverTime() throws Exception { Thread.sleep(110); Object f = ts.getTarget(); - assertThat(e.equals(f)).as("E and F should be different").isFalse(); + assertThat(e).as("E and F should be different").isNotEqualTo(f); } diff --git a/spring-aop/src/test/java/test/annotation/EmptySpringAnnotation.java b/spring-aop/src/test/java/test/annotation/EmptySpringAnnotation.java index fcb55a8330ab..45ea71a809bf 100644 --- a/spring-aop/src/test/java/test/annotation/EmptySpringAnnotation.java +++ b/spring-aop/src/test/java/test/annotation/EmptySpringAnnotation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2006 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/test/java/test/annotation/transaction/Tx.java b/spring-aop/src/test/java/test/annotation/transaction/Tx.java index bf7c9daa5c02..57e7de7136a7 100644 --- a/spring-aop/src/test/java/test/annotation/transaction/Tx.java +++ b/spring-aop/src/test/java/test/annotation/transaction/Tx.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2006 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..0489d68bb0c3 --- /dev/null +++ b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 +import java.time.LocalDateTime + +/** + * 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) + } + + @Test // gh-35487 + fun jvmDefault() { + val proxyFactory = ProxyFactory() + proxyFactory.setTarget(AddressRepo()) + proxyFactory.proxy + } + + + open class MyKotlinBean { + + open fun capitalize(value: String) = value.uppercase() + + open fun uncheckedException() { + throw IllegalStateException() + } + + open fun checkedException() { + throw CheckedException() + } + } + + class CheckedException() : Exception() + + open class AddressRepo(): CrudRepo + + interface CrudRepo { + fun save(e: E): E { + return e + } + fun delete(id: ID): Long { + return 0L + } + } + + data class Address( + val id: Int = 0, + val street: String, + val version: Int = 0, + val createdAt: LocalDateTime? = null, + val updatedAt: LocalDateTime? = null, + ) + +} 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 index 6e079a70276e..81bae7b58447 100644 --- a/spring-aop/src/test/kotlin/org/springframework/aop/framework/CoroutinesUtilsTests.kt +++ b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CoroutinesUtilsTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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 @@ 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 @@ -37,39 +36,31 @@ class CoroutinesUtilsTests { fun awaitSingleNonNullValue() { val value = "foo" val continuation = Continuation(CoroutineName("test")) { } - runBlocking { - assertThat(CoroutinesUtils.awaitSingleOrNull(value, continuation)).isEqualTo(value) - } + 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() - } + 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) - } + assertThat(CoroutinesUtils.awaitSingleOrNull(Mono.just(value), continuation)).isEqualTo(value) } @Test @Suppress("UNCHECKED_CAST") - fun flow() { + suspend 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) - } + 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 a3c54130560c..a233abfe4c76 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-2024 the original author or authors. + * Copyright 2002-present the original author 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", Any::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/CountingAfterReturningAdvice.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingAfterReturningAdvice.java index 8cfab3ca0127..4e8f109a32b7 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingAfterReturningAdvice.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingAfterReturningAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingBeforeAdvice.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingBeforeAdvice.java index bf931f6bdfaa..c2e3363e7514 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingBeforeAdvice.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingBeforeAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..1112f0d19218 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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 java.util.HashMap; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** - * Abstract superclass for counting advices etc. + * Abstract superclass for counting advice, etc. * * @author Rod Johnson * @author Chris Beams @@ -62,7 +62,7 @@ public int getCalls() { */ @Override public boolean equals(@Nullable Object other) { - return (other != null && other.getClass() == this.getClass()); + return (other != null && getClass() == other.getClass()); } @Override diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MyThrowsHandler.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MyThrowsHandler.java index e718663d4de8..20d531e31361 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MyThrowsHandler.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MyThrowsHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/TimestampIntroductionAdvisor.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/TimestampIntroductionAdvisor.java index 5321e5d38110..082d79ec84e8 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/TimestampIntroductionAdvisor.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/TimestampIntroductionAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonExpressions.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonExpressions.java index 477ceb1173fa..939f04470408 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonPointcuts.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonPointcuts.java index 8421c8a1d2cc..e222481a61a6 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonPointcuts.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/CommonPointcuts.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/PerTargetAspect.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/PerTargetAspect.java index 0f9a34dde096..c33fb7ddf7c3 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/PerTargetAspect.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/PerTargetAspect.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/PerThisAspect.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/PerThisAspect.java index fa7b5133e440..1230dbf8b0f5 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/PerThisAspect.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/PerThisAspect.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/TwoAdviceAspect.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/TwoAdviceAspect.java index 449bbdc71463..d4d88783cc3e 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/TwoAdviceAspect.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/aspectj/TwoAdviceAspect.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..7a595c17cd3f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,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/interceptor/SerializableNopInterceptor.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/SerializableNopInterceptor.java index 25d9546c53a0..d3bb5885bf77 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/SerializableNopInterceptor.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/SerializableNopInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/TimestampIntroductionInterceptor.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/TimestampIntroductionInterceptor.java index 24761350132a..628be65c3057 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/TimestampIntroductionInterceptor.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/TimestampIntroductionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/DefaultLockable.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/DefaultLockable.java index b495d2dc8848..654d8d1a5d85 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/DefaultLockable.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/DefaultLockable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 6c08f24ce2fc..24475d78fad0 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixinAdvisor.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixinAdvisor.java index 21a7255c8822..9a2c79bc7a13 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixinAdvisor.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockMixinAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/Lockable.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/Lockable.java index 83d0ea05f0d9..36c53a7ce273 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/Lockable.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/Lockable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockedException.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockedException.java index 04a8268e8f05..ecabc2caef26 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockedException.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/mixin/LockedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java index ee0370d73520..41311c7b2db6 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/spring-aspects.gradle b/spring-aspects/spring-aspects.gradle index 6ca211a8dd6b..a79c4c8d5fc7 100644 --- a/spring-aspects/spring-aspects.gradle +++ b/spring-aspects/spring-aspects.gradle @@ -3,15 +3,15 @@ description = "Spring Aspects" apply plugin: "io.freefair.aspectj" compileAspectj { - sourceCompatibility "17" - targetCompatibility "17" + sourceCompatibility = "17" + targetCompatibility = "17" ajcOptions { compilerArgs += "-parameters" } } compileTestAspectj { - sourceCompatibility "17" - targetCompatibility "17" + sourceCompatibility = "17" + targetCompatibility = "17" ajcOptions { compilerArgs += "-parameters" } diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj index 19ba5e5b0530..0fdc00b75c97 100644 --- a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractInterfaceDrivenDependencyInjectionAspect.aj b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractInterfaceDrivenDependencyInjectionAspect.aj index 6e049a58a045..8a69dd3995a9 100644 --- a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractInterfaceDrivenDependencyInjectionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractInterfaceDrivenDependencyInjectionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AnnotationBeanConfigurerAspect.aj b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AnnotationBeanConfigurerAspect.aj index 0c7472383dde..b29203d6679e 100644 --- a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AnnotationBeanConfigurerAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AnnotationBeanConfigurerAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java index 02e0d6391fb4..8cee7d842eb6 100644 --- a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/GenericInterfaceDrivenDependencyInjectionAspect.aj b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/GenericInterfaceDrivenDependencyInjectionAspect.aj index 867ecffb4fba..a8e2c2665f65 100644 --- a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/GenericInterfaceDrivenDependencyInjectionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/GenericInterfaceDrivenDependencyInjectionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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/AbstractCacheAspect.aj b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj index ae6d6a34895b..1228ddbfaf73 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnnotationCacheAspect.aj b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnnotationCacheAspect.aj index 671b3a66696e..559d7f6460df 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnnotationCacheAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnnotationCacheAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnyThrow.java b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnyThrow.java index ff0ec4c8623f..4425cb2978ac 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnyThrow.java +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnyThrow.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java index e48a0f669a74..4d7cfd89019a 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java index 3468889517f5..6851dfd42048 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj b/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj index ebda3f292cc9..62be8a3e47cc 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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/EnableSpringConfigured.java b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java index 0f362d91e8f0..a0e7aa79b820 100644 --- a/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java +++ b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/SpringConfiguredConfiguration.java b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/SpringConfiguredConfiguration.java index eb369ca49c58..1dbf3f9f76a0 100644 --- a/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/SpringConfiguredConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/SpringConfiguredConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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/AbstractAsyncExecutionAspect.aj b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj index eed22f42743c..1ac886616d0c 100644 --- a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj index 46c1b4530aec..b43a1378f5a2 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-present the original author 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/AspectJAsyncConfiguration.java b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java index c6cdada36567..e1afd9767f12 100644 --- a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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/AbstractTransactionAspect.aj b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj index 782ca35e0777..bf99ed99a92e 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AnnotationTransactionAspect.aj b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AnnotationTransactionAspect.aj index bdaae703b0dc..cbd1103631be 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AnnotationTransactionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AnnotationTransactionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java index fc51788cd015..87f7a8717ada 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java index 4e82c4524a7a..b004d0095f5c 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj index 8b374ea0d86e..1895cf1be13c 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 a37e14101732..5752686207ac 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/CodeStyleAspect.aj b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/CodeStyleAspect.aj index 0ba9e0dd5a6a..10ba507358a6 100644 --- a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/CodeStyleAspect.aj +++ b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/CodeStyleAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/ShouldBeConfiguredBySpring.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/ShouldBeConfiguredBySpring.java index 586d9364b5b8..22e014241c5a 100644 --- a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/ShouldBeConfiguredBySpring.java +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/ShouldBeConfiguredBySpring.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 f47eb7ba3c53..6e74bc5fe073 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 053417e21878..7bb4bb885021 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java index 8cec3f65ea54..f5fadec2d154 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 Phillip Webb * @author Stephane Nicoll */ -public abstract class AbstractCacheAnnotationTests { +abstract class AbstractCacheAnnotationTests { protected ConfigurableApplicationContext ctx; @@ -67,7 +67,7 @@ public abstract class AbstractCacheAnnotationTests { @BeforeEach - public void setup() { + void setup() { this.ctx = getApplicationContext(); this.cs = ctx.getBean("service", CacheableService.class); this.ccs = ctx.getBean("classService", CacheableService.class); @@ -78,7 +78,7 @@ public void setup() { } @AfterEach - public void close() { + void close() { if (this.ctx != null) { this.ctx.close(); } @@ -555,133 +555,134 @@ protected void testMultiConditionalCacheAndEvict(CacheableService service) { assertThat(secondary.get(key2)).isNull(); } + @Test - void testCacheable() { + void cacheable() { testCacheable(this.cs); } @Test - void testCacheableNull() { + void cacheableNull() { testCacheableNull(this.cs); } @Test - void testCacheableSync() { + void cacheableSync() { testCacheableSync(this.cs); } @Test - void testCacheableSyncNull() { + void cacheableSyncNull() { testCacheableSyncNull(this.cs); } @Test - void testEvict() { + void evict() { testEvict(this.cs, true); } @Test - void testEvictEarly() { + void evictEarly() { testEvictEarly(this.cs); } @Test - void testEvictWithException() { + void evictWithException() { testEvictException(this.cs); } @Test - void testEvictAll() { + void evictAll() { testEvictAll(this.cs, true); } @Test - void testEvictAllEarly() { + void evictAllEarly() { testEvictAllEarly(this.cs); } @Test - void testEvictWithKey() { + void evictWithKey() { testEvictWithKey(this.cs); } @Test - void testEvictWithKeyEarly() { + void evictWithKeyEarly() { testEvictWithKeyEarly(this.cs); } @Test - void testConditionalExpression() { + void conditionalExpression() { testConditionalExpression(this.cs); } @Test - void testConditionalExpressionSync() { + void conditionalExpressionSync() { testConditionalExpressionSync(this.cs); } @Test - void testUnlessExpression() { + void unlessExpression() { testUnlessExpression(this.cs); } @Test - void testClassCacheUnlessExpression() { + void classCacheUnlessExpression() { testUnlessExpression(this.cs); } @Test - void testKeyExpression() { + void keyExpression() { testKeyExpression(this.cs); } @Test - void testVarArgsKey() { + void varArgsKey() { testVarArgsKey(this.cs); } @Test - void testClassCacheCacheable() { + void classCacheCacheable() { testCacheable(this.ccs); } @Test - void testClassCacheEvict() { + void classCacheEvict() { testEvict(this.ccs, true); } @Test - void testClassEvictEarly() { + void classEvictEarly() { testEvictEarly(this.ccs); } @Test - void testClassEvictAll() { + void classEvictAll() { testEvictAll(this.ccs, true); } @Test - void testClassEvictWithException() { + void classEvictWithException() { testEvictException(this.ccs); } @Test - void testClassCacheEvictWithWKey() { + void classCacheEvictWithWKey() { testEvictWithKey(this.ccs); } @Test - void testClassEvictWithKeyEarly() { + void classEvictWithKeyEarly() { testEvictWithKeyEarly(this.ccs); } @Test - void testNullValue() { + void nullValue() { testNullValue(this.cs); } @Test - void testClassNullValue() { + void classNullValue() { Object key = new Object(); assertThat(this.ccs.nullValue(key)).isNull(); int nr = this.ccs.nullInvocations().intValue(); @@ -694,27 +695,27 @@ void testClassNullValue() { } @Test - void testMethodName() { + void methodName() { testMethodName(this.cs, "name"); } @Test - void testClassMethodName() { + void classMethodName() { testMethodName(this.ccs, "nametestCache"); } @Test - void testRootVars() { + void rootVars() { testRootVars(this.cs); } @Test - void testClassRootVars() { + void classRootVars() { testRootVars(this.ccs); } @Test - void testCustomKeyGenerator() { + void customKeyGenerator() { Object param = new Object(); Object r1 = this.cs.customKeyGenerator(param); assertThat(this.cs.customKeyGenerator(param)).isSameAs(r1); @@ -725,14 +726,14 @@ void testCustomKeyGenerator() { } @Test - void testUnknownCustomKeyGenerator() { + void unknownCustomKeyGenerator() { Object param = new Object(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> this.cs.unknownCustomKeyGenerator(param)); } @Test - void testCustomCacheManager() { + void customCacheManager() { CacheManager customCm = this.ctx.getBean("customCacheManager", CacheManager.class); Object key = new Object(); Object r1 = this.cs.customCacheManager(key); @@ -743,139 +744,139 @@ void testCustomCacheManager() { } @Test - void testUnknownCustomCacheManager() { + void unknownCustomCacheManager() { Object param = new Object(); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> this.cs.unknownCustomCacheManager(param)); } @Test - void testNullArg() { + void nullArg() { testNullArg(this.cs); } @Test - void testClassNullArg() { + void classNullArg() { testNullArg(this.ccs); } @Test - void testCheckedException() { + void checkedException() { testCheckedThrowable(this.cs); } @Test - void testClassCheckedException() { + void classCheckedException() { testCheckedThrowable(this.ccs); } @Test - void testCheckedExceptionSync() { + void checkedExceptionSync() { testCheckedThrowableSync(this.cs); } @Test - void testClassCheckedExceptionSync() { + void classCheckedExceptionSync() { testCheckedThrowableSync(this.ccs); } @Test - void testUncheckedException() { + void uncheckedException() { testUncheckedThrowable(this.cs); } @Test - void testClassUncheckedException() { + void classUncheckedException() { testUncheckedThrowable(this.ccs); } @Test - void testUncheckedExceptionSync() { + void uncheckedExceptionSync() { testUncheckedThrowableSync(this.cs); } @Test - void testClassUncheckedExceptionSync() { + void classUncheckedExceptionSync() { testUncheckedThrowableSync(this.ccs); } @Test - void testUpdate() { + void update() { testCacheUpdate(this.cs); } @Test - void testClassUpdate() { + void classUpdate() { testCacheUpdate(this.ccs); } @Test - void testConditionalUpdate() { + void conditionalUpdate() { testConditionalCacheUpdate(this.cs); } @Test - void testClassConditionalUpdate() { + void classConditionalUpdate() { testConditionalCacheUpdate(this.ccs); } @Test - void testMultiCache() { + void multiCache() { testMultiCache(this.cs); } @Test - void testClassMultiCache() { + void classMultiCache() { testMultiCache(this.ccs); } @Test - void testMultiEvict() { + void multiEvict() { testMultiEvict(this.cs); } @Test - void testClassMultiEvict() { + void classMultiEvict() { testMultiEvict(this.ccs); } @Test - void testMultiPut() { + void multiPut() { testMultiPut(this.cs); } @Test - void testClassMultiPut() { + void classMultiPut() { testMultiPut(this.ccs); } @Test - void testPutRefersToResult() { + void putRefersToResult() { testPutRefersToResult(this.cs); } @Test - void testClassPutRefersToResult() { + void classPutRefersToResult() { testPutRefersToResult(this.ccs); } @Test - void testMultiCacheAndEvict() { + void multiCacheAndEvict() { testMultiCacheAndEvict(this.cs); } @Test - void testClassMultiCacheAndEvict() { + void classMultiCacheAndEvict() { testMultiCacheAndEvict(this.ccs); } @Test - void testMultiConditionalCacheAndEvict() { + void multiConditionalCacheAndEvict() { testMultiConditionalCacheAndEvict(this.cs); } @Test - void testClassMultiConditionalCacheAndEvict() { + void classMultiConditionalCacheAndEvict() { 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 ffeab37c4213..feee32a95df3 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ protected ConfigurableApplicationContext getApplicationContext() { } @Test - void testKeyStrategy() { + void keyStrategy() { 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 df8e1d3588a8..49a76fe95e79 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.cache.aspectj; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -51,6 +51,7 @@ */ class AspectJEnableCachingIsolatedTests { + @AutoClose private ConfigurableApplicationContext ctx; @@ -58,23 +59,16 @@ private void load(Class... config) { this.ctx = new AnnotationConfigApplicationContext(config); } - @AfterEach - public void closeContext() { - if (this.ctx != null) { - this.ctx.close(); - } - } - @Test - void testKeyStrategy() { + void keyStrategy() { load(EnableCachingConfig.class); AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); assertThat(aspect.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator", KeyGenerator.class)); } @Test - void testCacheErrorHandler() { + void cacheErrorHandler() { load(EnableCachingConfig.class); AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); assertThat(aspect.getErrorHandler()).isSameAs(this.ctx.getBean("errorHandler", CacheErrorHandler.class)); @@ -95,7 +89,10 @@ void multipleCacheManagerBeans() { } catch (NoUniqueBeanDefinitionException ex) { assertThat(ex.getMessage()).contains( - "no CacheResolver specified and expected a single CacheManager bean, but found 2: [cm1,cm2]"); + "no CacheResolver specified and expected single matching CacheManager but found 2") + .contains("cm1", "cm2"); + assertThat(ex.getNumberOfBeansFound()).isEqualTo(2); + assertThat(ex.getBeanNamesFound()).containsExactlyInAnyOrder("cm1", "cm2"); } } @@ -126,7 +123,7 @@ void noCacheManagerBeans() { @Test @Disabled("AspectJ has some sort of caching that makes this one fail") - public void emptyConfigSupport() { + void emptyConfigSupport() { load(EmptyConfigSupportConfig.class); AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); assertThat(aspect.getCacheResolver()).isNotNull(); @@ -281,4 +278,5 @@ public CacheResolver cacheResolver() { return new NamedCacheResolver(cacheManager(), "foo"); } } + } 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 8b3b440782ce..f60dba20d4fb 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java index a106633859c1..17c7839d82be 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java index a2879aa57639..86e3a4664cd4 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java index 447c4cc0f617..f86274ad59a8 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java index 0b004f46b910..d9378e94c134 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java index 3ec6212bac60..1f56fe3a31fc 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java index 47a3a83a34a1..7ee51944583a 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java b/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java index 0219086ed48d..1c37f42146e5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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 49544d99e051..e49c1917afb6 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java index 6cf02e31db2c..615ad1144a2e 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,6 @@ import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.ReflectionUtils; -import org.springframework.util.concurrent.ListenableFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; @@ -47,7 +46,7 @@ * @author Stephane Nicoll */ @EnabledForTestGroups(LONG_RUNNING) -public class AnnotationAsyncExecutionAspectTests { +class AnnotationAsyncExecutionAspectTests { private static final long WAIT_TIME = 1000; //milliseconds @@ -57,7 +56,7 @@ public class AnnotationAsyncExecutionAspectTests { @BeforeEach - public void setUp() { + void setUp() { executor = new CountingExecutor(); AnnotationAsyncExecutionAspect.aspectOf().setExecutor(executor); } @@ -136,10 +135,7 @@ void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws InterruptedExcepti assertThat(defaultThread.get()).isNotEqualTo(Thread.currentThread()); assertThat(defaultThread.get().getName()).doesNotStartWith("e1-"); - ListenableFuture e1Thread = obj.e1Work(); - assertThat(e1Thread.get().getName()).startsWith("e1-"); - - CompletableFuture e1OtherThread = obj.e1OtherWork(); + CompletableFuture e1OtherThread = obj.e1Work(); assertThat(e1OtherThread.get().getName()).startsWith("e1-"); } @@ -269,12 +265,7 @@ public Future defaultWork() { } @Async("e1") - public ListenableFuture e1Work() { - return new AsyncResult<>(Thread.currentThread()); - } - - @Async("e1") - public CompletableFuture e1OtherWork() { + public CompletableFuture e1Work() { return CompletableFuture.completedFuture(Thread.currentThread()); } } diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java index ee8799a68f15..11d62beb3ae5 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,13 @@ class AnnotationDrivenBeanDefinitionParserTests { private ConfigurableApplicationContext context; @BeforeEach - public void setup() { + void setup() { this.context = new ClassPathXmlApplicationContext( "annotationDrivenContext.xml", AnnotationDrivenBeanDefinitionParserTests.class); } @AfterEach - public void after() { + void after() { if (this.context != null) { this.context.close(); } @@ -56,7 +56,7 @@ void asyncAspectRegistered() { @Test @SuppressWarnings("rawtypes") - public void asyncPostProcessorExecutorReference() { + void asyncPostProcessorExecutorReference() { Object executor = context.getBean("testExecutor"); Object aspect = context.getBean(TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME); assertThat(((Supplier) new DirectFieldAccessor(aspect).getPropertyValue("defaultExecutor")).get()).isSameAs(executor); @@ -64,7 +64,7 @@ public void asyncPostProcessorExecutorReference() { @Test @SuppressWarnings("rawtypes") - public void asyncPostProcessorExceptionHandlerReference() { + void asyncPostProcessorExceptionHandlerReference() { Object exceptionHandler = context.getBean("testExceptionHandler"); Object aspect = context.getBean(TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME); assertThat(((Supplier) new DirectFieldAccessor(aspect).getPropertyValue("exceptionHandler")).get()).isSameAs(exceptionHandler); 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 86d575febd6a..ec3c61c9a5f4 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java index d0d824c1db73..dd1e65802732 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java index 12c5db965d87..e72f027ba936 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ITransactional.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ITransactional.java index e553c94e59fb..a56c8705bd31 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ITransactional.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ITransactional.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java index 0a0280aa8b89..903599fdce71 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,13 +36,13 @@ * @author Stephane Nicoll */ @SpringJUnitConfig(JtaTransactionAspectsTests.Config.class) -public class JtaTransactionAspectsTests { +class JtaTransactionAspectsTests { @Autowired private CallCountingTransactionManager txManager; @BeforeEach - public void setUp() { + void setUp() { this.txManager.clear(); } diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/MethodAnnotationOnClassWithNoInterface.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/MethodAnnotationOnClassWithNoInterface.java index 35067b4ca536..8f1c2502d3a8 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/MethodAnnotationOnClassWithNoInterface.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/MethodAnnotationOnClassWithNoInterface.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java index d706674d7204..75297ae5723f 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,13 +50,13 @@ class TransactionAspectTests { @BeforeEach - public void initContext() { + void initContext() { AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager); } @Test - void testCommitOnAnnotatedClass() throws Throwable { + void commitOnAnnotatedClass() throws Throwable { txManager.clear(); assertThat(txManager.begun).isEqualTo(0); annotationOnlyOnClassWithNoInterface.echo(null); diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionalAnnotationOnlyOnClassWithNoInterface.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionalAnnotationOnlyOnClassWithNoInterface.java index d9eeb7bbcb74..53620cc5df1d 100644 --- a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionalAnnotationOnlyOnClassWithNoInterface.java +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionalAnnotationOnlyOnClassWithNoInterface.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 bc5e5258f825..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 @@ -21,8 +21,10 @@ - + - + + + 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/AbstractPropertyAccessorBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java index 6cdd116b4cb0..bd25e48614b0 100644 --- a/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java +++ b/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/jmh/java/org/springframework/beans/BeanUtilsBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/BeanUtilsBenchmark.java index b656648d8754..7fad50ff78cf 100644 --- a/spring-beans/src/jmh/java/org/springframework/beans/BeanUtilsBenchmark.java +++ b/spring-beans/src/jmh/java/org/springframework/beans/BeanUtilsBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/jmh/java/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark.java index 3214290d1870..2de3a2eb9ed3 100644 --- a/spring-beans/src/jmh/java/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark.java +++ b/spring-beans/src/jmh/java/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java index 58e6b215bba9..2e5d9edbe1de 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-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt b/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt index 3d06b4245423..f03500c4ec5b 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index 04fc76399ad1..8253a9fe9cd9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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; @@ -76,19 +76,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA */ private static final Log logger = LogFactory.getLog(AbstractNestablePropertyAccessor.class); - 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; /** @@ -159,21 +154,6 @@ protected AbstractNestablePropertyAccessor(Object object, String nestedPath, Abs } - /** - * Specify a limit for array and collection auto-growing. - *

Default is unlimited on a plain accessor. - */ - public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) { - this.autoGrowCollectionLimit = autoGrowCollectionLimit; - } - - /** - * Return the limit for array and collection auto-growing. - */ - public int getAutoGrowCollectionLimit() { - return this.autoGrowCollectionLimit; - } - /** * Switch the target object, replacing the cached introspection results only * if the class of the new object is different to that of the replaced object. @@ -291,7 +271,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 +279,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(); + if (arrayIndex >= length && arrayIndex < getAutoGrowCollectionLimit()) { Object newArray = Array.newInstance(componentType, arrayIndex + 1); System.arraycopy(propValue, 0, newArray, 0, length); int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); @@ -328,7 +307,7 @@ else if (propValue instanceof List list) { Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), requiredType.getResolvableType().resolve(), requiredType); int size = list.size(); - if (index >= size && index < this.autoGrowCollectionLimit) { + if (index >= size && index < getAutoGrowCollectionLimit()) { for (int i = size; i < index; i++) { try { list.add(null); @@ -474,7 +453,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); @@ -488,8 +467,10 @@ private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) } @Override - @Nullable - public Class getPropertyType(String propertyName) throws BeansException { + public @Nullable Class getPropertyType(String propertyName) throws BeansException { + if (this.wrappedObject == null) { + return null; + } try { PropertyHandler ph = getPropertyHandler(propertyName); if (ph != null) { @@ -516,8 +497,7 @@ public Class getPropertyType(String propertyName) throws BeansException { } @Override - @Nullable - public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { + public @Nullable TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { try { AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); String finalPath = getFinalPath(nestedPa, propertyName); @@ -580,8 +560,7 @@ public boolean isWritableProperty(String propertyName) { return false; } - @Nullable - private Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, + private @Nullable Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class requiredType, @Nullable TypeDescriptor td) throws TypeMismatchException { @@ -601,8 +580,7 @@ private Object convertIfNecessary(@Nullable String propertyName, @Nullable Objec } } - @Nullable - protected Object convertForProperty( + protected @Nullable Object convertForProperty( String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) throws TypeMismatchException { @@ -610,16 +588,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); @@ -658,6 +634,14 @@ else if (value instanceof List list) { growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1); value = list.get(index); } + else if (value instanceof Map map) { + Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); + // IMPORTANT: Do not pass full property name in here - property editors + // must not kick in for map keys but rather only for map values. + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); + Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); + value = map.get(convertedMapKey); + } else if (value instanceof Iterable iterable) { // Apply index to Iterator in case of a Set/Collection/Iterable. int index = Integer.parseInt(key); @@ -685,14 +669,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 + @@ -734,8 +710,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)); @@ -747,8 +722,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. @@ -770,7 +744,7 @@ private Object growArrayIfNecessary(Object array, int index, String name) { return array; } int length = Array.getLength(array); - if (index >= length && index < this.autoGrowCollectionLimit) { + if (index >= length && index < getAutoGrowCollectionLimit()) { Class componentType = array.getClass().componentType(); Object newArray = Array.newInstance(componentType, index + 1); System.arraycopy(array, 0, newArray, 0, length); @@ -794,7 +768,7 @@ private void growCollectionIfNecessary(Collection collection, int index, return; } int size = collection.size(); - if (index >= size && index < this.autoGrowCollectionLimit) { + if (index >= size && index < getAutoGrowCollectionLimit()) { Class elementType = ph.getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); if (elementType != null) { for (int i = collection.size(); i < index + 1; i++) { @@ -904,16 +878,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); @@ -937,6 +902,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 @@ -956,8 +939,8 @@ private PropertyTokenHolder getPropertyNameTokens(String propertyName) { actualName = propertyName.substring(0, keyStart); } String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd); - if (key.length() > 1 && (key.startsWith("'") && key.endsWith("'")) || - (key.startsWith("\"") && key.endsWith("\""))) { + if (key.length() > 1 && ((key.startsWith("'") && key.endsWith("'")) || + (key.startsWith("\"") && key.endsWith("\"")))) { key = key.substring(1, key.length() - 1); } keys.add(key); @@ -1016,8 +999,7 @@ public String toString() { */ protected abstract static class PropertyHandler { - @Nullable - private final Class propertyType; + private final @Nullable Class propertyType; private final boolean readable; @@ -1029,8 +1011,7 @@ public PropertyHandler(@Nullable Class propertyType, boolean readable, boolea this.writable = writable; } - @Nullable - public Class getPropertyType() { + public @Nullable Class getPropertyType() { return this.propertyType; } @@ -1058,11 +1039,9 @@ public TypeDescriptor getCollectionType(int nestingLevel) { return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric()); } - @Nullable - public abstract TypeDescriptor nested(int level); + public abstract @Nullable TypeDescriptor nested(int level); - @Nullable - public abstract Object getValue() throws Exception; + public abstract @Nullable Object getValue() throws Exception; public abstract void setValue(@Nullable Object value) throws Exception; @@ -1086,8 +1065,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..7f00c47698d8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import java.util.List; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract implementation of the {@link PropertyAccessor} interface. @@ -40,6 +40,8 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl private boolean autoGrowNestedPaths = false; + private int autoGrowCollectionLimit = Integer.MAX_VALUE; + boolean suppressNotWritablePropertyException = false; @@ -63,6 +65,16 @@ public boolean isAutoGrowNestedPaths() { return this.autoGrowNestedPaths; } + @Override + public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) { + this.autoGrowCollectionLimit = autoGrowCollectionLimit; + } + + @Override + public int getAutoGrowCollectionLimit() { + return this.autoGrowCollectionLimit; + } + @Override public void setPropertyValue(PropertyValue pv) throws BeansException { @@ -139,8 +151,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 +165,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..1c04e0013104 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,11 @@ import java.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 a07cae6d3b84..4b5591f14aa7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Exception thrown when instantiation of a bean failed. @@ -33,11 +33,9 @@ public class BeanInstantiationException extends FatalBeanException { private final Class beanClass; - @Nullable - private final Constructor constructor; + private final @Nullable Constructor constructor; - @Nullable - private final Method constructingMethod; + private final @Nullable Method constructingMethod; /** @@ -106,8 +104,7 @@ public Class getBeanClass() { * factory method or in case of default instantiation * @since 4.3 */ - @Nullable - public Constructor getConstructor() { + public @Nullable Constructor getConstructor() { return this.constructor; } @@ -117,8 +114,7 @@ public Constructor getConstructor() { * or {@code null} in case of constructor-based instantiation * @since 4.3 */ - @Nullable - public Method getConstructingMethod() { + public @Nullable Method getConstructingMethod() { return this.constructingMethod; } diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java index f5c8a854ad48..932843f65fd8 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-present the original author 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..6298e5c55663 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttributeAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttributeAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..107bdfb208ae 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataElement.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataElement.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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; /** * 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 cedf0408f2a3..152c6feaf3f9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,19 +33,19 @@ import java.util.Set; import kotlin.jvm.JvmClassMappingKt; +import kotlin.jvm.internal.DefaultConstructorMarker; import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; import kotlin.reflect.full.KClasses; import kotlin.reflect.jvm.KCallablesJvm; import kotlin.reflect.jvm.ReflectJvmMapping; +import org.jspecify.annotations.Nullable; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; -import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -70,9 +71,6 @@ */ public abstract class BeanUtils { - private static final ParameterNameDiscoverer parameterNameDiscoverer = - new DefaultParameterNameDiscoverer(); - private static final Set> unknownEditorTypes = Collections.newSetFromMap(new ConcurrentReferenceHashMap<>(64)); @@ -86,6 +84,8 @@ public abstract class BeanUtils { double.class, 0D, char.class, '\0'); + private static final boolean KOTLIN_REFLECT_PRESENT = KotlinDetector.isKotlinReflectPresent(); + /** * Convenience method to instantiate a class using its no-arg constructor. @@ -93,10 +93,9 @@ public abstract class BeanUtils { * @return the new instance * @throws BeanInstantiationException if the bean cannot be instantiated * @see Class#newInstance() - * @deprecated as of Spring 5.0, following the deprecation of - * {@link Class#newInstance()} in JDK 9 + * @deprecated following the deprecation of {@link Class#newInstance()} in JDK 9 */ - @Deprecated + @Deprecated(since = "5.0") public static T instantiate(Class clazz) throws BeanInstantiationException { Assert.notNull(clazz, "Class must not be null"); if (clazz.isInterface()) { @@ -125,7 +124,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 */ @@ -181,11 +180,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 (KOTLIN_REFLECT_PRESENT && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { return KotlinDelegate.instantiateClass(ctor, args); } else { @@ -195,7 +194,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]; @@ -224,9 +223,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 @@ -248,7 +248,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]; } } @@ -268,18 +268,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 (KOTLIN_REFLECT_PRESENT && 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; } @@ -297,8 +310,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); } @@ -318,8 +330,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); } @@ -346,8 +357,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); @@ -369,8 +379,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); @@ -389,8 +398,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; @@ -441,8 +449,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('('); @@ -495,8 +502,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); } @@ -509,8 +515,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()); } @@ -524,8 +529,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) { @@ -538,15 +542,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; } @@ -560,7 +563,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; } } @@ -593,7 +596,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); @@ -649,11 +652,15 @@ 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() : + DefaultParameterNameDiscoverer.getSharedInstance().getParameterNames(ctor)); Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor); - Assert.state(paramNames.length == ctor.getParameterCount(), + int parameterCount = (KOTLIN_REFLECT_PRESENT && KotlinDelegate.hasDefaultConstructorMarker(ctor) ? + ctor.getParameterCount() - 1 : ctor.getParameterCount()); + Assert.state(paramNames.length == parameterCount, () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); return paramNames; } @@ -787,7 +794,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"); @@ -864,8 +871,7 @@ private static class KotlinDelegate { * https://kotlinlang.org/docs/reference/classes.html#constructors */ @SuppressWarnings("unchecked") - @Nullable - public static Constructor findPrimaryConstructor(Class clazz) { + public static @Nullable Constructor findPrimaryConstructor(Class clazz) { try { KClass kClass = JvmClassMappingKt.getKotlinClass(clazz); KFunction primaryCtor = KClasses.getPrimaryConstructor(kClass); @@ -896,7 +902,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); @@ -923,6 +929,11 @@ public static T instantiateClass(Constructor ctor, Object... args) } return kotlinConstructor.callBy(argParameters); } + + public static boolean hasDefaultConstructorMarker(Constructor ctor) { + int parameterCount = ctor.getParameterCount(); + return parameterCount > 0 && ctor.getParameters()[parameterCount -1].getType() == DefaultConstructorMarker.class; + } } } diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtilsRuntimeHints.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtilsRuntimeHints.java index 3fc2e306739a..0c7ea3ec583d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtilsRuntimeHints.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtilsRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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/BeanWrapper.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java index 798191cc55d7..ffd6a5a672c6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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 @@ public interface BeanWrapper extends ConfigurablePropertyAccessor { /** - * Specify a limit for array and collection auto-growing. + * Specify a limit for array and collection/set/list auto-growing. *

Default is unlimited on a plain BeanWrapper. * @since 4.1 */ diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index 93a9724d4420..09cb89f49b20 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,11 @@ import java.lang.reflect.Method; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -64,8 +64,7 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements * Cached introspections results for this object, to prevent encountering * the cost of JavaBeans introspection every time. */ - @Nullable - private CachedIntrospectionResults cachedIntrospectionResults; + private @Nullable CachedIntrospectionResults cachedIntrospectionResults; /** @@ -178,8 +177,7 @@ private CachedIntrospectionResults getCachedIntrospectionResults() { * @return the new value, possibly the result of type conversion * @throws TypeMismatchException if type conversion failed */ - @Nullable - public Object convertForProperty(@Nullable Object value, String propertyName) throws TypeMismatchException { + public @Nullable Object convertForProperty(@Nullable Object value, String propertyName) throws TypeMismatchException { CachedIntrospectionResults cachedIntrospectionResults = getCachedIntrospectionResults(); PropertyDescriptor pd = cachedIntrospectionResults.getPropertyDescriptor(propertyName); if (pd == null) { @@ -191,8 +189,7 @@ public Object convertForProperty(@Nullable Object value, String propertyName) th } @Override - @Nullable - protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) { + protected @Nullable PropertyHandler getLocalPropertyHandler(String propertyName) { PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName); return (pd != null ? new BeanPropertyHandler((GenericTypeAwarePropertyDescriptor) pd) : null); } @@ -261,14 +258,12 @@ public TypeDescriptor getCollectionType(int nestingLevel) { } @Override - @Nullable - public TypeDescriptor nested(int level) { + public @Nullable TypeDescriptor nested(int level) { return this.pd.getTypeDescriptor().nested(level); } @Override - @Nullable - public Object getValue() throws Exception { + public @Nullable Object getValue() throws Exception { Method readMethod = this.pd.getReadMethod(); Assert.state(readMethod != null, "No read method available"); ReflectionUtils.makeAccessible(readMethod); diff --git a/spring-beans/src/main/java/org/springframework/beans/BeansException.java b/spring-beans/src/main/java/org/springframework/beans/BeansException.java index f3816a16db50..05f7cc762c10 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeansException.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeansException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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 21acc8141be7..0c673e6cc131 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,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; @@ -107,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. @@ -283,14 +283,14 @@ private CachedIntrospectionResults(Class beanClass) throws BeansException { } // Explicitly check implemented interfaces for setter/getter methods as well, - // in particular for Java 8 default methods... + // in particular for interface default methods. Class currClass = beanClass; while (currClass != null && currClass != Object.class) { introspectInterfaces(beanClass, currClass, readMethodNames); 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); @@ -375,8 +375,7 @@ Class getBeanClass() { return this.beanInfo.getBeanDescriptor().getBeanClass(); } - @Nullable - PropertyDescriptor getPropertyDescriptor(String name) { + @Nullable PropertyDescriptor getPropertyDescriptor(String name) { PropertyDescriptor pd = this.propertyDescriptors.get(name); if (pd == null && StringUtils.hasLength(name)) { // Same lenient fallback checking as in Property... diff --git a/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java index 38c5d26a4573..bcf6c3c86d20 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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 @@ -63,13 +63,28 @@ public interface ConfigurablePropertyAccessor extends PropertyAccessor, Property *

If {@code true}, a {@code null} path location will be populated * with a default object value and traversed instead of resulting in a * {@link NullValueInNestedPathException}. - *

Default is {@code false} on a plain PropertyAccessor instance. + *

Default is {@code false} on a plain accessor. + * @since 4.1 */ void setAutoGrowNestedPaths(boolean autoGrowNestedPaths); /** * Return whether "auto-growing" of nested paths has been activated. + * @since 4.1 */ boolean isAutoGrowNestedPaths(); + /** + * Specify a limit for array and collection auto-growing. + *

Default is unlimited on a plain accessor. + * @since 7.1 + */ + void setAutoGrowCollectionLimit(int autoGrowCollectionLimit); + + /** + * Return the limit for array and collection auto-growing. + * @since 7.1 + */ + int getAutoGrowCollectionLimit(); + } 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..695b8d08e467 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ConversionNotSupportedException.java +++ b/spring-beans/src/main/java/org/springframework/beans/ConversionNotSupportedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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.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..99f39bcf37a6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,12 +17,14 @@ package org.springframework.beans; import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; 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 +73,7 @@ protected DirectFieldAccessor(Object object, String nestedPath, DirectFieldAcces @Override - @Nullable - protected FieldPropertyHandler getLocalPropertyHandler(String propertyName) { + protected @Nullable PropertyHandler getLocalPropertyHandler(String propertyName) { FieldPropertyHandler propertyHandler = this.fieldMap.get(propertyName); if (propertyHandler == null) { Field field = ReflectionUtils.findField(getWrappedClass(), propertyName); @@ -132,19 +133,17 @@ 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()); } - catch (IllegalAccessException ex) { + catch (IllegalAccessException | InaccessibleObjectException ex) { throw new InvalidPropertyException(getWrappedClass(), this.field.getName(), "Field is not accessible", ex); } @@ -156,7 +155,7 @@ public void setValue(@Nullable Object value) throws Exception { ReflectionUtils.makeAccessible(this.field); this.field.set(getWrappedInstance(), value); } - catch (IllegalAccessException ex) { + catch (IllegalAccessException | InaccessibleObjectException ex) { throw new InvalidPropertyException(getWrappedClass(), this.field.getName(), "Field is not accessible", ex); } 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 804ef9d21b31..cba10a190431 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,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(); @@ -221,7 +220,7 @@ private String propertyNameFor(Method method) { */ @Override public PropertyDescriptor[] getPropertyDescriptors() { - return this.propertyDescriptors.toArray(new PropertyDescriptor[0]); + return this.propertyDescriptors.toArray(PropertyDescriptorUtils.EMPTY_PROPERTY_DESCRIPTOR_ARRAY); } @Override @@ -265,17 +264,13 @@ public MethodDescriptor[] getMethodDescriptors() { */ static class SimplePropertyDescriptor extends PropertyDescriptor { - @Nullable - private Method readMethod; + private @Nullable Method readMethod; - @Nullable - private Method writeMethod; + private @Nullable Method writeMethod; - @Nullable - private Class propertyType; + private @Nullable Class propertyType; - @Nullable - private Class propertyEditorClass; + private @Nullable Class propertyEditorClass; public SimplePropertyDescriptor(PropertyDescriptor original) throws IntrospectionException { this(original.getName(), original.getReadMethod(), original.getWriteMethod()); @@ -292,8 +287,7 @@ public SimplePropertyDescriptor(String propertyName, @Nullable Method readMethod } @Override - @Nullable - public Method getReadMethod() { + public @Nullable Method getReadMethod() { return this.readMethod; } @@ -303,8 +297,7 @@ public void setReadMethod(@Nullable Method readMethod) { } @Override - @Nullable - public Method getWriteMethod() { + public @Nullable Method getWriteMethod() { return this.writeMethod; } @@ -314,8 +307,7 @@ public void setWriteMethod(@Nullable Method writeMethod) { } @Override - @Nullable - public Class getPropertyType() { + public @Nullable Class getPropertyType() { if (this.propertyType == null) { try { this.propertyType = PropertyDescriptorUtils.findPropertyType(this.readMethod, this.writeMethod); @@ -328,8 +320,7 @@ public Class getPropertyType() { } @Override - @Nullable - public Class getPropertyEditorClass() { + public @Nullable Class getPropertyEditorClass() { return this.propertyEditorClass; } @@ -362,26 +353,19 @@ public String toString() { */ static class SimpleIndexedPropertyDescriptor extends IndexedPropertyDescriptor { - @Nullable - private Method readMethod; + private @Nullable Method readMethod; - @Nullable - private Method writeMethod; + private @Nullable Method writeMethod; - @Nullable - private Class propertyType; + private @Nullable Class propertyType; - @Nullable - private Method indexedReadMethod; + private @Nullable Method indexedReadMethod; - @Nullable - private Method indexedWriteMethod; + private @Nullable Method indexedWriteMethod; - @Nullable - private Class indexedPropertyType; + private @Nullable Class indexedPropertyType; - @Nullable - private Class propertyEditorClass; + private @Nullable Class propertyEditorClass; public SimpleIndexedPropertyDescriptor(IndexedPropertyDescriptor original) throws IntrospectionException { this(original.getName(), original.getReadMethod(), original.getWriteMethod(), @@ -404,8 +388,7 @@ public SimpleIndexedPropertyDescriptor(String propertyName, @Nullable Method rea } @Override - @Nullable - public Method getReadMethod() { + public @Nullable Method getReadMethod() { return this.readMethod; } @@ -415,8 +398,7 @@ public void setReadMethod(@Nullable Method readMethod) { } @Override - @Nullable - public Method getWriteMethod() { + public @Nullable Method getWriteMethod() { return this.writeMethod; } @@ -426,8 +408,7 @@ public void setWriteMethod(@Nullable Method writeMethod) { } @Override - @Nullable - public Class getPropertyType() { + public @Nullable Class getPropertyType() { if (this.propertyType == null) { try { this.propertyType = PropertyDescriptorUtils.findPropertyType(this.readMethod, this.writeMethod); @@ -440,8 +421,7 @@ public Class getPropertyType() { } @Override - @Nullable - public Method getIndexedReadMethod() { + public @Nullable Method getIndexedReadMethod() { return this.indexedReadMethod; } @@ -451,8 +431,7 @@ public void setIndexedReadMethod(@Nullable Method indexedReadMethod) throws Intr } @Override - @Nullable - public Method getIndexedWriteMethod() { + public @Nullable Method getIndexedWriteMethod() { return this.indexedWriteMethod; } @@ -462,8 +441,7 @@ public void setIndexedWriteMethod(@Nullable Method indexedWriteMethod) throws In } @Override - @Nullable - public Class getIndexedPropertyType() { + public @Nullable Class getIndexedPropertyType() { if (this.indexedPropertyType == null) { try { this.indexedPropertyType = PropertyDescriptorUtils.findIndexedPropertyType( @@ -477,8 +455,7 @@ public Class getIndexedPropertyType() { } @Override - @Nullable - public Class getPropertyEditorClass() { + public @Nullable Class getPropertyEditorClass() { return this.propertyEditorClass; } diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java index 5f41742632d9..8bffe2550067 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.lang.reflect.Method; import org.springframework.core.Ordered; -import org.springframework.lang.NonNull; /** * Extension of {@link StandardBeanInfoFactory} that supports "non-standard" @@ -29,7 +28,8 @@ * (package-visible) {@code ExtendedBeanInfo} implementation. * *

To be configured via a {@code META-INF/spring.factories} file with the following content: - * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory} + * + *

{@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory} * *

Ordered at {@link Ordered#LOWEST_PRECEDENCE} to allow other user-defined * {@link BeanInfoFactory} types to take precedence. @@ -43,7 +43,6 @@ public class ExtendedBeanInfoFactory extends StandardBeanInfoFactory { @Override - @NonNull public 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..3fec4a35e72f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/FatalBeanException.java +++ b/spring-beans/src/main/java/org/springframework/beans/FatalBeanException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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 a8247f6e421c..0a95e82c70f2 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,34 +47,25 @@ final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor { private final Class beanClass; - @Nullable - private final Method readMethod; + private final @Nullable Method readMethod; - @Nullable - private final Method writeMethod; + private final @Nullable Method writeMethod; - @Nullable - private Set ambiguousWriteMethods; + private @Nullable Set ambiguousWriteMethods; private volatile boolean ambiguousWriteMethodsLogged; - @Nullable - private MethodParameter writeMethodParameter; + private @Nullable MethodParameter writeMethodParameter; - @Nullable - private volatile ResolvableType writeMethodType; + private volatile @Nullable ResolvableType writeMethodType; - @Nullable - private ResolvableType readMethodType; + private @Nullable ResolvableType readMethodType; - @Nullable - private volatile TypeDescriptor typeDescriptor; + private volatile @Nullable TypeDescriptor typeDescriptor; - @Nullable - private Class propertyType; + private @Nullable Class propertyType; - @Nullable - private final Class propertyEditorClass; + private final @Nullable Class propertyEditorClass; public GenericTypeAwarePropertyDescriptor(Class beanClass, String propertyName, @@ -136,14 +127,12 @@ public Class getBeanClass() { } @Override - @Nullable - public Method getReadMethod() { + public @Nullable Method getReadMethod() { return this.readMethod; } @Override - @Nullable - public Method getWriteMethod() { + public @Nullable Method getWriteMethod() { return this.writeMethod; } @@ -158,8 +147,7 @@ public Method getWriteMethodForActualAccess() { return this.writeMethod; } - @Nullable - public Method getWriteMethodFallback(@Nullable Class valueType) { + public @Nullable Method getWriteMethodFallback(@Nullable Class valueType) { if (this.ambiguousWriteMethods != null) { for (Method method : this.ambiguousWriteMethods) { Class paramType = method.getParameterTypes()[0]; @@ -171,8 +159,7 @@ public Method getWriteMethodFallback(@Nullable Class valueType) { return null; } - @Nullable - public Method getUniqueWriteMethodFallback() { + public @Nullable Method getUniqueWriteMethodFallback() { if (this.ambiguousWriteMethods != null && this.ambiguousWriteMethods.size() == 1) { return this.ambiguousWriteMethods.iterator().next(); } @@ -213,14 +200,12 @@ public TypeDescriptor getTypeDescriptor() { } @Override - @Nullable - public Class getPropertyType() { + public @Nullable Class getPropertyType() { return this.propertyType; } @Override - @Nullable - public Class getPropertyEditorClass() { + public @Nullable Class getPropertyEditorClass() { return this.propertyEditorClass; } diff --git a/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java b/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java index c0d0f50adbbb..e9a5501def14 100644 --- a/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java +++ b/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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; /** * 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..9356e5a2c75a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/Mergeable.java +++ b/spring-beans/src/main/java/org/springframework/beans/Mergeable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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; /** * Interface representing an object whose value set can be merged with diff --git a/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java index 327643cbbf3f..c45ca60f5827 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-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,14 @@ import java.beans.PropertyChangeEvent; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Thrown when a bean property getter or setter method throws an exception, * analogous to an InvocationTargetException. * * @author Rod Johnson + * @author Juergen Hoeller */ @SuppressWarnings("serial") public class MethodInvocationException extends PropertyAccessException { @@ -41,7 +42,9 @@ public class MethodInvocationException extends PropertyAccessException { * @param cause the Throwable raised by the invoked method */ public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, @Nullable Throwable cause) { - super(propertyChangeEvent, "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception", cause); + super(propertyChangeEvent, + "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception: " + cause, + cause); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java index f52780e0ec9d..f78e16af92a7 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,8 @@ import java.util.Spliterator; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -43,10 +44,12 @@ @SuppressWarnings("serial") public class MutablePropertyValues implements PropertyValues, Serializable { + private static final PropertyValue[] EMPTY_PROPERTY_VALUE_ARRAY = new PropertyValue[0]; + + private final List propertyValueList; - @Nullable - private Set processedProperties; + private @Nullable Set processedProperties; private volatile boolean converted; @@ -264,12 +267,11 @@ public Stream stream() { @Override public PropertyValue[] getPropertyValues() { - return this.propertyValueList.toArray(new PropertyValue[0]); + return this.propertyValueList.toArray(EMPTY_PROPERTY_VALUE_ARRAY); } @Override - @Nullable - public PropertyValue getPropertyValue(String propertyName) { + public @Nullable PropertyValue getPropertyValue(String propertyName) { for (PropertyValue pv : this.propertyValueList) { if (pv.getName().equals(propertyName)) { return pv; @@ -286,8 +288,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/NotReadablePropertyException.java b/spring-beans/src/main/java/org/springframework/beans/NotReadablePropertyException.java index 52d3befde496..76ffc70bcf40 100644 --- a/spring-beans/src/main/java/org/springframework/beans/NotReadablePropertyException.java +++ b/spring-beans/src/main/java/org/springframework/beans/NotReadablePropertyException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java b/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java index 79e017e89ac7..5da2527ecdda 100644 --- a/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java +++ b/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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; /** * 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/NullValueInNestedPathException.java b/spring-beans/src/main/java/org/springframework/beans/NullValueInNestedPathException.java index efb4d3a47299..289017d4fc44 100644 --- a/spring-beans/src/main/java/org/springframework/beans/NullValueInNestedPathException.java +++ b/spring-beans/src/main/java/org/springframework/beans/NullValueInNestedPathException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java index 7789437b551a..fea4e0e02bb5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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..ee707c2f35b6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,9 @@ import java.util.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/PropertyAccessorFactory.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorFactory.java index 78e8ec6e616f..1dc58c71d95c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java index 465b3ff8b1d8..f527f83bd93b 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-2023 the original author or authors. + * Copyright 2002-present the original author 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 @@ -154,7 +154,8 @@ public static String canonicalPropertyName(@Nullable String propertyName) { PropertyAccessor.PROPERTY_KEY_SUFFIX, keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length()); if (keyEnd != -1) { String key = sb.substring(keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length(), keyEnd); - if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) { + if (key.length() > 1 && ((key.startsWith("'") && key.endsWith("'")) || + (key.startsWith("\"") && key.endsWith("\"")))) { sb.delete(keyStart + 1, keyStart + 2); sb.delete(keyEnd - 2, keyEnd - 1); keyEnd = keyEnd - 2; @@ -173,8 +174,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..b16c65444516 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyBatchUpdateException.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyBatchUpdateException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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.io.PrintWriter; import java.util.StringJoiner; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -73,8 +74,7 @@ public final PropertyAccessException[] getPropertyAccessExceptions() { /** * Return the exception for this field, or {@code null} if there isn't any. */ - @Nullable - public PropertyAccessException getPropertyAccessException(String propertyName) { + public @Nullable PropertyAccessException getPropertyAccessException(String propertyName) { for (PropertyAccessException pae : this.propertyAccessExceptions) { if (ObjectUtils.nullSafeEquals(propertyName, pae.getPropertyName())) { return pae; diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java index 90c2aef90275..7bc37b37eb7b 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.IntrospectionException; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; @@ -26,7 +27,9 @@ import java.util.Map; import java.util.TreeMap; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.ResolvableType; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -35,6 +38,7 @@ * * @author Chris Beams * @author Juergen Hoeller + * @author Sam Brannen */ abstract class PropertyDescriptorUtils { @@ -47,6 +51,12 @@ abstract class PropertyDescriptorUtils { *

This just supports the basic JavaBeans conventions, without indexed * properties or any customizers, and without other BeanInfo metadata. * For standard JavaBeans introspection, use the JavaBeans Introspector. + *

Note that, in contrast to the standard {@link java.beans.Introspector}, + * this method does support a static {@code set} method as the write method + * for a property, resulting in a write-only property if no corresponding + * instance {@code get}/{@code is} method is present. Static {@code get} and + * {@code is} methods, on the other hand, are never considered read methods + * for a property, aligning with standard JavaBeans introspection. * @param beanClass the target class to introspect * @return a collection of property descriptors * @throws IntrospectionException from introspecting the given bean class @@ -68,11 +78,13 @@ public static Collection determineBasicProperties( setter = true; nameIndex = 3; } - else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != void.class) { + else if (methodName.startsWith("get") && method.getParameterCount() == 0 && + method.getReturnType() != void.class && !Modifier.isStatic(method.getModifiers())) { setter = false; nameIndex = 3; } - else if (methodName.startsWith("is") && method.getParameterCount() == 0 && method.getReturnType() == boolean.class) { + else if (methodName.startsWith("is") && method.getParameterCount() == 0 && + method.getReturnType() == boolean.class && !Modifier.isStatic(method.getModifiers())) { setter = false; nameIndex = 2; } @@ -88,25 +100,17 @@ else if (methodName.startsWith("is") && method.getParameterCount() == 0 && metho BasicPropertyDescriptor pd = pdMap.get(propertyName); if (pd != null) { if (setter) { - Method writeMethod = pd.getWriteMethod(); - if (writeMethod == null || - writeMethod.getParameterTypes()[0].isAssignableFrom(method.getParameterTypes()[0])) { - pd.setWriteMethod(method); - } - else { - pd.addWriteMethod(method); - } + pd.addWriteMethod(method); } else { Method readMethod = pd.getReadMethod(); - if (readMethod == null || - (readMethod.getReturnType() == method.getReturnType() && method.getName().startsWith("is"))) { + if (readMethod == null || readMethod.getReturnType().isAssignableFrom(method.getReturnType())) { pd.setReadMethod(method); } } } else { - pd = new BasicPropertyDescriptor(propertyName, (!setter ? method : null), (setter ? method : null)); + pd = new BasicPropertyDescriptor(propertyName, beanClass, (!setter ? method : null), (setter ? method : null)); pdMap.put(propertyName, pd); } } @@ -141,8 +145,7 @@ public static void copyNonMethodProperties(PropertyDescriptor source, PropertyDe /** * See {@link java.beans.PropertyDescriptor#findPropertyType}. */ - @Nullable - public static Class findPropertyType(@Nullable Method readMethod, @Nullable Method writeMethod) + public static @Nullable Class findPropertyType(@Nullable Method readMethod, @Nullable Method writeMethod) throws IntrospectionException { Class propertyType = null; @@ -186,8 +189,7 @@ else if (params[0].isAssignableFrom(propertyType)) { /** * See {@link java.beans.IndexedPropertyDescriptor#findIndexedPropertyType}. */ - @Nullable - public static Class findIndexedPropertyType(String name, @Nullable Class propertyType, + public static @Nullable Class findIndexedPropertyType(String name, @Nullable Class propertyType, @Nullable Method indexedReadMethod, @Nullable Method indexedWriteMethod) throws IntrospectionException { Class indexedPropertyType = null; @@ -264,18 +266,19 @@ public static boolean equals(PropertyDescriptor pd, PropertyDescriptor otherPd) */ private static class BasicPropertyDescriptor extends PropertyDescriptor { - @Nullable - private Method readMethod; + private final Class beanClass; + + private @Nullable Method readMethod; - @Nullable - private Method writeMethod; + private @Nullable Method writeMethod; - private final List alternativeWriteMethods = new ArrayList<>(); + private final List candidateWriteMethods = new ArrayList<>(); - public BasicPropertyDescriptor(String propertyName, @Nullable Method readMethod, @Nullable Method writeMethod) + public BasicPropertyDescriptor(String propertyName, Class beanClass, @Nullable Method readMethod, @Nullable Method writeMethod) throws IntrospectionException { super(propertyName, readMethod, writeMethod); + this.beanClass = beanClass; } @Override @@ -284,8 +287,7 @@ public void setReadMethod(@Nullable Method readMethod) { } @Override - @Nullable - public Method getReadMethod() { + public @Nullable Method getReadMethod() { return this.readMethod; } @@ -294,27 +296,48 @@ public void setWriteMethod(@Nullable Method writeMethod) { this.writeMethod = writeMethod; } - public void addWriteMethod(Method writeMethod) { + void addWriteMethod(Method writeMethod) { + // Since setWriteMethod() is invoked from the PropertyDescriptor(String, Method, Method) + // constructor, this.writeMethod may be non-null. if (this.writeMethod != null) { - this.alternativeWriteMethods.add(this.writeMethod); + this.candidateWriteMethods.add(this.writeMethod); this.writeMethod = null; } - this.alternativeWriteMethods.add(writeMethod); + this.candidateWriteMethods.add(writeMethod); } @Override - @Nullable - public Method getWriteMethod() { - if (this.writeMethod == null && !this.alternativeWriteMethods.isEmpty()) { - if (this.readMethod == null) { - return this.alternativeWriteMethods.get(0); + public @Nullable Method getWriteMethod() { + if (this.writeMethod == null && !this.candidateWriteMethods.isEmpty()) { + if (this.readMethod == null || this.candidateWriteMethods.size() == 1) { + this.writeMethod = this.candidateWriteMethods.get(0); } else { - for (Method method : this.alternativeWriteMethods) { - if (this.readMethod.getReturnType().isAssignableFrom(method.getParameterTypes()[0])) { + Class resolvedReadType = + ResolvableType.forMethodReturnType(this.readMethod, this.beanClass).toClass(); + for (Method method : this.candidateWriteMethods) { + // 1) Check for an exact match against the resolved types. + Class resolvedWriteType = + ResolvableType.forMethodParameter(method, 0, this.beanClass).toClass(); + if (resolvedReadType.equals(resolvedWriteType)) { this.writeMethod = method; break; } + + // 2) Check if the candidate write method's parameter type is compatible with + // the read method's return type. + Class parameterType = method.getParameterTypes()[0]; + if (this.readMethod.getReturnType().isAssignableFrom(parameterType)) { + // If we haven't yet found a compatible write method, or if the current + // candidate's parameter type is a subtype of the previous candidate's + // parameter type, track the current candidate as the write method. + if (this.writeMethod == null || + this.writeMethod.getParameterTypes()[0].isAssignableFrom(parameterType)) { + this.writeMethod = method; + // We do not "break" here, since we need to compare the current candidate + // with all remaining candidates. + } + } } } } @@ -322,5 +345,4 @@ public Method getWriteMethod() { } } - } 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..9080e1c8ff7a 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-present the original author 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..27e65f78b228 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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.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 af3e0cc00c5c..da6b9232919f 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-present the original author 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,23 @@ */ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry { - @Nullable - private ConversionService conversionService; + private @Nullable ConversionService conversionService; private boolean defaultEditorsActive = false; private boolean configValueEditorsActive = false; - @Nullable - private Map, PropertyEditor> defaultEditors; + private @Nullable PropertyEditorRegistrar defaultEditorRegistrar; - @Nullable - private Map, PropertyEditor> overriddenDefaultEditors; + private @Nullable Map, PropertyEditor> defaultEditors; - @Nullable - private Map, PropertyEditor> customEditors; + private @Nullable Map, PropertyEditor> overriddenDefaultEditors; - @Nullable - private Map customEditorsForPath; + private @Nullable Map, PropertyEditor> customEditors; - @Nullable - private Map, PropertyEditor> customEditorCache; + private @Nullable Map customEditorsForPath; + + private @Nullable Map, PropertyEditor> customEditorCache; /** @@ -126,8 +122,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 +150,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,12 +186,13 @@ public void overrideDefaultEditor(Class requiredType, PropertyEditor property * @return the default editor, or {@code null} if none found * @see #registerDefaultEditors */ - @Nullable - @SuppressWarnings("NullAway") - public PropertyEditor getDefaultEditor(Class requiredType) { + public @Nullable PropertyEditor getDefaultEditor(Class requiredType) { if (!this.defaultEditorsActive) { return null; } + if (this.overriddenDefaultEditors == null && this.defaultEditorRegistrar != null) { + this.defaultEditorRegistrar.registerCustomEditors(this); + } if (this.overriddenDefaultEditors != null) { PropertyEditor editor = this.overriddenDefaultEditors.get(requiredType); if (editor != null) { @@ -191,7 +200,7 @@ public PropertyEditor getDefaultEditor(Class requiredType) { } } if (this.defaultEditors == null) { - createDefaultEditors(); + this.defaultEditors = createDefaultEditors(); } return this.defaultEditors.get(requiredType); } @@ -199,75 +208,77 @@ public PropertyEditor getDefaultEditor(Class requiredType) { /** * Actually register the default editors for this registry instance. */ - private void createDefaultEditors() { - this.defaultEditors = new HashMap<>(64); + private Map, PropertyEditor> createDefaultEditors() { + Map, PropertyEditor> defaultEditors = new HashMap<>(64); // Simple editors, without parameterization capabilities. // The JDK does not contain a default editor for any of these target types. - this.defaultEditors.put(Charset.class, new CharsetEditor()); - this.defaultEditors.put(Class.class, new ClassEditor()); - this.defaultEditors.put(Class[].class, new ClassArrayEditor()); - this.defaultEditors.put(Currency.class, new CurrencyEditor()); - this.defaultEditors.put(File.class, new FileEditor()); - this.defaultEditors.put(InputStream.class, new InputStreamEditor()); - this.defaultEditors.put(InputSource.class, new InputSourceEditor()); - this.defaultEditors.put(Locale.class, new LocaleEditor()); - this.defaultEditors.put(Path.class, new PathEditor()); - this.defaultEditors.put(Pattern.class, new PatternEditor()); - this.defaultEditors.put(Properties.class, new PropertiesEditor()); - this.defaultEditors.put(Reader.class, new ReaderEditor()); - this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor()); - this.defaultEditors.put(TimeZone.class, new TimeZoneEditor()); - this.defaultEditors.put(URI.class, new URIEditor()); - this.defaultEditors.put(URL.class, new URLEditor()); - this.defaultEditors.put(UUID.class, new UUIDEditor()); - this.defaultEditors.put(ZoneId.class, new ZoneIdEditor()); + defaultEditors.put(Charset.class, new CharsetEditor()); + defaultEditors.put(Class.class, new ClassEditor()); + defaultEditors.put(Class[].class, new ClassArrayEditor()); + defaultEditors.put(Currency.class, new CurrencyEditor()); + defaultEditors.put(File.class, new FileEditor()); + defaultEditors.put(InputStream.class, new InputStreamEditor()); + defaultEditors.put(InputSource.class, new InputSourceEditor()); + defaultEditors.put(Locale.class, new LocaleEditor()); + defaultEditors.put(Path.class, new PathEditor()); + defaultEditors.put(Pattern.class, new PatternEditor()); + defaultEditors.put(Properties.class, new PropertiesEditor()); + defaultEditors.put(Reader.class, new ReaderEditor()); + defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor()); + defaultEditors.put(TimeZone.class, new TimeZoneEditor()); + defaultEditors.put(URI.class, new URIEditor()); + defaultEditors.put(URL.class, new URLEditor()); + defaultEditors.put(UUID.class, new UUIDEditor()); + defaultEditors.put(ZoneId.class, new ZoneIdEditor()); // Default instances of collection editors. // Can be overridden by registering custom instances of those as custom editors. - this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class)); - this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class)); - this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class)); - this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class)); - this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class)); + defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class)); + defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class)); + defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class)); + defaultEditors.put(List.class, new CustomCollectionEditor(List.class)); + defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class)); // Default editors for primitive arrays. - this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor()); - this.defaultEditors.put(char[].class, new CharArrayPropertyEditor()); + defaultEditors.put(byte[].class, new ByteArrayPropertyEditor()); + defaultEditors.put(char[].class, new CharArrayPropertyEditor()); // The JDK does not contain a default editor for char! - this.defaultEditors.put(char.class, new CharacterEditor(false)); - this.defaultEditors.put(Character.class, new CharacterEditor(true)); + defaultEditors.put(char.class, new CharacterEditor(false)); + defaultEditors.put(Character.class, new CharacterEditor(true)); // Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor. - this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false)); - this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true)); + defaultEditors.put(boolean.class, new CustomBooleanEditor(false)); + defaultEditors.put(Boolean.class, new CustomBooleanEditor(true)); // The JDK does not contain default editors for number wrapper types! // Override JDK primitive number editors with our own CustomNumberEditor. - this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false)); - this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true)); - this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false)); - this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true)); - this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false)); - this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true)); - this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false)); - this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true)); - this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false)); - this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true)); - this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false)); - this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true)); - this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true)); - this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true)); + defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false)); + defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true)); + defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false)); + defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true)); + defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false)); + defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true)); + defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false)); + defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true)); + defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false)); + defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true)); + defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false)); + defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true)); + defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true)); + defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true)); // Only register config value editors if explicitly requested. if (this.configValueEditorsActive) { StringArrayPropertyEditor sae = new StringArrayPropertyEditor(); - this.defaultEditors.put(String[].class, sae); - this.defaultEditors.put(short[].class, sae); - this.defaultEditors.put(int[].class, sae); - this.defaultEditors.put(long[].class, sae); + defaultEditors.put(String[].class, sae); + defaultEditors.put(short[].class, sae); + defaultEditors.put(int[].class, sae); + defaultEditors.put(long[].class, sae); } + + return defaultEditors; } /** @@ -312,8 +323,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) { @@ -372,8 +382,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; } @@ -383,8 +392,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); @@ -398,8 +406,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; } @@ -438,8 +445,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) { @@ -526,8 +532,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; @@ -538,13 +543,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/PropertyMatches.java b/spring-beans/src/main/java/org/springframework/beans/PropertyMatches.java index 659f84ff2a80..773c45a6046d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyMatches.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyMatches.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java b/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java index 00f567b0f67b..3dfc79d907fa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..790fce9a42e7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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 @@ 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/PropertyValuesEditor.java b/spring-beans/src/main/java/org/springframework/beans/PropertyValuesEditor.java index 4d112b4718e5..cbdcb8162431 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyValuesEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValuesEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java index 75c9a699bb69..750e5cfb368c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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,8 @@ import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; import java.beans.SimpleBeanInfo; -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} @@ -33,7 +31,8 @@ *

Used by default in 6.0 through direct invocation from {@link CachedIntrospectionResults}. * Potentially configured via a {@code META-INF/spring.factories} file with the following content, * overriding other custom {@code org.springframework.beans.BeanInfoFactory} declarations: - * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.SimpleBeanInfoFactory} + * + *

{@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.SimpleBeanInfoFactory} * *

Ordered at {@code Ordered.LOWEST_PRECEDENCE - 1} to override {@link ExtendedBeanInfoFactory} * (registered by default in 5.3) if necessary while still allowing other user-defined @@ -47,10 +46,9 @@ class SimpleBeanInfoFactory implements BeanInfoFactory, Ordered { @Override - @NonNull public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { - Collection pds = - PropertyDescriptorUtils.determineBasicProperties(beanClass); + PropertyDescriptor[] pds = PropertyDescriptorUtils.determineBasicProperties(beanClass) + .toArray(PropertyDescriptorUtils.EMPTY_PROPERTY_DESCRIPTOR_ARRAY); return new SimpleBeanInfo() { @Override @@ -59,7 +57,7 @@ public BeanDescriptor getBeanDescriptor() { } @Override public PropertyDescriptor[] getPropertyDescriptors() { - return pds.toArray(PropertyDescriptorUtils.EMPTY_PROPERTY_DESCRIPTOR_ARRAY); + return pds; } }; } diff --git a/spring-beans/src/main/java/org/springframework/beans/SimpleTypeConverter.java b/spring-beans/src/main/java/org/springframework/beans/SimpleTypeConverter.java index 1313f4e91904..9a4d26f451e4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/SimpleTypeConverter.java +++ b/spring-beans/src/main/java/org/springframework/beans/SimpleTypeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java index d93d8d6a6905..c6aa5f906e8d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,14 +22,14 @@ import org.springframework.core.Ordered; import org.springframework.core.SpringProperties; -import org.springframework.lang.NonNull; /** * {@link BeanInfoFactory} implementation that performs standard * {@link java.beans.Introspector} inspection. * *

To be configured via a {@code META-INF/spring.factories} file with the following content: - * {@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.StandardBeanInfoFactory} + * + *

{@code org.springframework.beans.BeanInfoFactory=org.springframework.beans.StandardBeanInfoFactory} * *

Ordered at {@link Ordered#LOWEST_PRECEDENCE} to allow other user-defined * {@link BeanInfoFactory} types to take precedence. @@ -66,7 +66,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..cf84d402b89b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,10 @@ import java.lang.reflect.Field; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; /** * Interface that defines type conversion methods. Typically (but not necessarily) @@ -51,8 +52,7 @@ public interface TypeConverter { * @see org.springframework.core.convert.ConversionService * @see org.springframework.core.convert.converter.Converter */ - @Nullable - T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException; + @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException; /** * Convert the value to the required type (if necessary from a String). @@ -70,8 +70,7 @@ public interface TypeConverter { * @see org.springframework.core.convert.ConversionService * @see org.springframework.core.convert.converter.Converter */ - @Nullable - T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException; /** @@ -90,8 +89,7 @@ T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType * @see org.springframework.core.convert.ConversionService * @see org.springframework.core.convert.converter.Converter */ - @Nullable - T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) + @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) throws TypeMismatchException; /** @@ -110,8 +108,7 @@ T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType * @see org.springframework.core.convert.ConversionService * @see org.springframework.core.convert.converter.Converter */ - @Nullable - default T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + default @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { throw new UnsupportedOperationException("TypeDescriptor resolution not supported"); diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java index f41724275445..db804b4a031f 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; @@ -60,8 +60,7 @@ class TypeConverterDelegate { private final PropertyEditorRegistrySupport propertyEditorRegistry; - @Nullable - private final Object targetObject; + private final @Nullable Object targetObject; /** @@ -93,8 +92,7 @@ public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistr * @return the new value, possibly the result of type conversion * @throws IllegalArgumentException if type conversion failed */ - @Nullable - public T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, + public @Nullable T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, Object newValue, @Nullable Class requiredType) throws IllegalArgumentException { return convertIfNecessary(propertyName, oldValue, newValue, requiredType, TypeDescriptor.valueOf(requiredType)); @@ -113,8 +111,7 @@ public T convertIfNecessary(@Nullable String propertyName, @Nullable Object * @throws IllegalArgumentException if type conversion failed */ @SuppressWarnings("unchecked") - @Nullable - public T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, + public @Nullable T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException { // Custom editor for this type? @@ -298,8 +295,13 @@ private Object attemptToConvertStringToEnum(Class requiredType, String trimme ClassLoader cl = this.targetObject.getClass().getClassLoader(); try { Class enumValueType = ClassUtils.forName(enumType, cl); - Field enumField = enumValueType.getField(fieldName); - convertedValue = enumField.get(null); + if (enumValueType.isEnum()) { + Field enumField = enumValueType.getField(fieldName); + convertedValue = enumField.get(null); + } + else if (logger.isTraceEnabled()) { + logger.trace("Specified enum class [" + enumType + "] is not a Java enum"); + } } catch (ClassNotFoundException ex) { if (logger.isTraceEnabled()) { @@ -316,8 +318,7 @@ private Object attemptToConvertStringToEnum(Class requiredType, String trimme if (convertedValue == currentConvertedValue) { // 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. + // with values defined as static fields. try { Field enumField = requiredType.getField(trimmedValue); ReflectionUtils.makeAccessible(enumField); @@ -337,8 +338,7 @@ private Object attemptToConvertStringToEnum(Class requiredType, String trimme * @param requiredType the type to find an editor for * @return the corresponding editor, or {@code null} if none */ - @Nullable - private PropertyEditor findDefaultEditor(@Nullable Class requiredType) { + private @Nullable PropertyEditor findDefaultEditor(@Nullable Class requiredType) { PropertyEditor editor = null; if (requiredType != null) { // No custom editor -> check BeanWrapperImpl's default editors. @@ -362,8 +362,7 @@ private PropertyEditor findDefaultEditor(@Nullable Class requiredType) { * @return the new value, possibly the result of type conversion * @throws IllegalArgumentException if type conversion failed */ - @Nullable - private Object doConvertValue(@Nullable Object oldValue, @Nullable Object newValue, + private @Nullable Object doConvertValue(@Nullable Object oldValue, @Nullable Object newValue, @Nullable Class requiredType, @Nullable PropertyEditor editor) { Object convertedValue = newValue; @@ -628,15 +627,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..8965709af9b2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,12 @@ import java.lang.reflect.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..af923e093091 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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/Aware.java b/spring-beans/src/main/java/org/springframework/beans/factory/Aware.java index 14ec4043f90b..b5ada4443f68 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/Aware.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/Aware.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanClassLoaderAware.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanClassLoaderAware.java index 179781a6d186..ccfea9c5491c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanClassLoaderAware.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanClassLoaderAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java index 9290a7153722..0d4cb5ce704e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,10 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.FatalBeanException; import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; /** * Exception thrown when a BeanFactory encounters an error when @@ -34,14 +35,11 @@ @SuppressWarnings("serial") public class BeanCreationException extends FatalBeanException { - @Nullable - private final String beanName; + private final @Nullable String beanName; - @Nullable - private final String resourceDescription; + private final @Nullable String resourceDescription; - @Nullable - private List relatedCauses; + private @Nullable List relatedCauses; /** @@ -120,16 +118,14 @@ public BeanCreationException(@Nullable String resourceDescription, String beanNa * Return the description of the resource that the bean * definition came from, if any. */ - @Nullable - public String getResourceDescription() { + public @Nullable String getResourceDescription() { return this.resourceDescription; } /** * Return the name of the bean requested, if any. */ - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } @@ -150,8 +146,7 @@ public void addRelatedCause(Throwable ex) { * Return the related causes, if any. * @return the array of related causes, or {@code null} if none */ - @Nullable - public Throwable[] getRelatedCauses() { + public Throwable @Nullable [] getRelatedCauses() { if (this.relatedCauses == null) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationNotAllowedException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationNotAllowedException.java index 45ff0c476f23..1c13b4f6a268 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationNotAllowedException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationNotAllowedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java index 5f5fc7b99d30..a25eb36920cc 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanDefinitionStoreException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanDefinitionStoreException.java index d807d5f90179..b35774b4862c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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/BeanExpressionException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanExpressionException.java index 8af52b8cfcea..667ca3e733d0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanExpressionException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanExpressionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java index 9e890e5d1ed8..b7ffe33ede7c 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-present the original author 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,11 @@ package org.springframework.beans.factory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; /** * The root interface for accessing a Spring bean container. @@ -36,7 +38,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 @@ -98,6 +100,7 @@ * @author Rod Johnson * @author Juergen Hoeller * @author Chris Beams + * @author Yanming Zhou * @since 13 April 2001 * @see BeanNameAware#setBeanName * @see BeanClassLoaderAware#setBeanClassLoader @@ -124,9 +127,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. @@ -166,6 +176,29 @@ public interface BeanFactory { */ T getBean(String name, Class requiredType) throws BeansException; + /** + * Return an instance, which may be shared or independent, of the specified bean. + *

Behaves the same as {@link #getBean(String)}, but provides a measure of type + * safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the + * required type. This means that ClassCastException can't be thrown on casting + * the result correctly, as can happen with {@link #getBean(String)}. + *

Translates aliases back to the corresponding canonical bean name. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the name of the bean to retrieve + * @param typeReference the reference to obtain type the bean must match + * @return an instance of the bean. + * Note that the return value will never be {@code null}. In case of a stub for + * {@code null} from a factory method having been resolved for the requested bean, a + * {@code BeanNotOfRequiredTypeException} against the NullBean stub will be raised. + * Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies. + * @throws NoSuchBeanDefinitionException if there is no such bean definition + * @throws BeanNotOfRequiredTypeException if the bean is not of the required type + * @throws BeansException if the bean could not be created + * @since 7.1 + * @see #getBean(String, Class) + */ + T getBean(String name, ParameterizedTypeReference typeReference) throws BeansException; + /** * Return an instance, which may be shared or independent, of the specified bean. *

Allows for specifying explicit constructor arguments / factory method arguments, @@ -182,7 +215,7 @@ public interface BeanFactory { * @throws BeansException if the bean could not be created * @since 2.5 */ - Object getBean(String name, Object... args) throws BeansException; + Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException; /** * Return the bean instance that uniquely matches the given object type, if any. @@ -220,7 +253,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 @@ -243,7 +276,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. @@ -256,6 +289,22 @@ public interface BeanFactory { */ ObjectProvider getBeanProvider(ResolvableType 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. + *

This is a variant of {@link #getBeanProvider(ResolvableType)} with a + * captured generic type for type-safe retrieval, typically used inline: + * {@code getBeanProvider(new ParameterizedTypeReference<>() {})} - and + * effectively equivalent to {@code getBeanProvider(ResolvableType.forType(...))}. + * @return a corresponding provider handle + * @param requiredType a captured generic type that the bean must match + * @since 7.0 + * @see #getBeanProvider(ResolvableType) + */ + ObjectProvider getBeanProvider(ParameterizedTypeReference requiredType); + /** * Does this bean factory contain a bean definition or externally registered singleton * instance with the given name? @@ -357,8 +406,7 @@ public interface BeanFactory { * @see #getBean * @see #isTypeMatch */ - @Nullable - Class getType(String name) throws NoSuchBeanDefinitionException; + @Nullable Class getType(String name) throws NoSuchBeanDefinitionException; /** * Determine the type of the bean with the given name. More specifically, @@ -378,8 +426,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/BeanFactoryAware.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryAware.java index 9812e0b0ad19..ea6b3023dc50 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryAware.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..774299160f1c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryInitializer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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..9969116c1857 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-present the original author 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/BeanInitializationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanInitializationException.java index e32973f653a4..cbac1c5e1186 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanInitializationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanInitializationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsAbstractException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsAbstractException.java index 69228a4bb70b..9e757213aae2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsAbstractException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsAbstractException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsNotAFactoryException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsNotAFactoryException.java index 213bcade4cf9..641187644623 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsNotAFactoryException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsNotAFactoryException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanNameAware.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNameAware.java index 994899c56c7e..49fbaa831a7f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanNameAware.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNameAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java index 1c122e17140c..bb51dd64b9b5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,17 @@ package org.springframework.beans.factory; +import java.lang.reflect.Type; + import org.springframework.beans.BeansException; -import org.springframework.util.ClassUtils; +import org.springframework.core.ResolvableType; /** * Thrown when a bean doesn't match the expected type. * * @author Rod Johnson * @author Juergen Hoeller + * @author Yanming Zhou */ @SuppressWarnings("serial") public class BeanNotOfRequiredTypeException extends BeansException { @@ -32,7 +35,7 @@ public class BeanNotOfRequiredTypeException extends BeansException { private final String beanName; /** The required type. */ - private final Class requiredType; + private final Type genericRequiredType; /** The offending type. */ private final Class actualType; @@ -46,10 +49,22 @@ public class BeanNotOfRequiredTypeException extends BeansException { * the expected type */ public BeanNotOfRequiredTypeException(String beanName, Class requiredType, Class actualType) { - super("Bean named '" + beanName + "' is expected to be of type '" + ClassUtils.getQualifiedName(requiredType) + - "' but was actually of type '" + ClassUtils.getQualifiedName(actualType) + "'"); + this(beanName, (Type) requiredType, actualType); + } + + /** + * Create a new BeanNotOfRequiredTypeException. + * @param beanName the name of the bean requested + * @param requiredType the required type + * @param actualType the actual type returned, which did not match + * the expected type + * @since 7.1 + */ + public BeanNotOfRequiredTypeException(String beanName, Type requiredType, Class actualType) { + super("Bean named '" + beanName + "' is expected to be of type '" + requiredType.getTypeName() + + "' but was actually of type '" + actualType.getTypeName() + "'"); this.beanName = beanName; - this.requiredType = requiredType; + this.genericRequiredType = requiredType; this.actualType = actualType; } @@ -65,7 +80,15 @@ public String getBeanName() { * Return the expected type for the bean. */ public Class getRequiredType() { - return this.requiredType; + return (this.genericRequiredType instanceof Class clazz ? clazz : ResolvableType.forType(this.genericRequiredType).toClass()); + } + + /** + * Return the expected generic type for the bean. + * @since 7.1 + */ + public Type getGenericRequiredType() { + return this.genericRequiredType; } /** 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..db73cd8b85ff --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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. Implementations use the + * {@link BeanRegistry} and {@link Environment} to register beans: + * + *

+ * 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!")));
+ *         }
+ *     }
+ * }
+ * + *

{@code BeanRegistrar} implementations are not Spring components: they must have + * a no-arg constructor and cannot rely on dependency injection or any other + * component-model feature. They can be used in two distinct ways depending on the + * application context setup. + * + *

With the {@code @Configuration} model

+ * + *

A {@code BeanRegistrar} must be imported via + * {@link org.springframework.context.annotation.Import @Import} on a + * {@link org.springframework.context.annotation.Configuration @Configuration} class: + * + *

+ * @Configuration
+ * @Import(MyBeanRegistrar.class)
+ * class MyConfiguration {
+ * }
+ * + *

This is the only mechanism that triggers bean registration in the annotation-based + * configuration model. Annotating an implementation with {@code @Configuration} or + * {@code @Component}, or returning an instance from a {@code @Bean} method, registers + * it as a bean but does not invoke its + * {@link #register(BeanRegistry, Environment) register} method. + * + *

When imported, the registrar is invoked in the order it is encountered during + * configuration class processing. It can therefore check for and build on beans that + * have already been defined, but has no visibility into beans that will be registered + * by classes processed later. + * + *

Programmatic usage

+ * + *

A {@code BeanRegistrar} can also be applied directly to a + * {@link org.springframework.context.support.GenericApplicationContext}: + * + *

+ * GenericApplicationContext context = new GenericApplicationContext();
+ * context.register(new MyBeanRegistrar());
+ * context.registerBean("myBean", MyBean.class);
+ * context.refresh();
+ * + *

This mode is primarily intended for fully programmatic application context setups. + * Registrars applied this way are invoked before any {@code @Configuration} class is + * processed. They can therefore observe beans registered programmatically (e.g., via + * one of the {@code GenericApplicationContext#registerBean} methods), but will + * not see any beans defined in {@code @Configuration} classes also + * registered with the context. + * + *

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 not 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..cadc878644da --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java @@ -0,0 +1,311 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.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 + * @author Juergen Hoeller + * @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 class, which will be instantiated using the + * related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. + *

For registering a bean with a generic type, consider + * {@link #registerBean(ParameterizedTypeReference)}. + * @param beanClass the class of the bean + * @return the generated bean name + * @see #registerBean(Class) + */ + String registerBean(Class beanClass); + + /** + * Register a bean from the given generics-containing type, which will be + * instantiated using the related + * {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. + * @param beanType the generics-containing type of the bean + * @return the generated bean name + */ + String registerBean(ParameterizedTypeReference beanType); + + /** + * Register a bean from the given 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. + *

For registering a bean with a generic type, consider + * {@link #registerBean(ParameterizedTypeReference, Consumer)}. + * @param beanClass the class of the bean + * @param customizer the 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 generics-containing type, 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 beanType the generics-containing type of the bean + * @param customizer the callback to customize other bean properties than the name + * @return the generated bean name + */ + String registerBean(ParameterizedTypeReference beanType, Consumer> customizer); + + /** + * Register a bean from the given class, which will be instantiated using the + * related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. + *

For registering a bean with a generic type, consider + * {@link #registerBean(String, ParameterizedTypeReference)}. + * @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 generics-containing type, which + * will be instantiated using the related + * {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. + * @param name the name of the bean + * @param beanType the generics-containing type of the bean + */ + void registerBean(String name, ParameterizedTypeReference beanType); + + /** + * Register a bean from the given 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. + *

For registering a bean with a generic type, consider + * {@link #registerBean(String, ParameterizedTypeReference, Consumer)}. + * @param name the name of the bean + * @param beanClass the class of the bean + * @param customizer the callback to customize other bean properties than the name + */ + void registerBean(String name, Class beanClass, Consumer> customizer); + + /** + * Register a bean from the given generics-containing type, 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 beanType the generics-containing type of the bean + * @param customizer the callback to customize other bean properties than the name + */ + void registerBean(String name, ParameterizedTypeReference beanType, Consumer> customizer); + + /** + * Determine whether a bean of the given name is already registered. + * @param name the name of the bean + * @since 7.1 + */ + boolean containsBean(String name); + + /** + * Determine whether a bean of the given type is already registered. + * @param beanType the type of the bean + * @since 7.1 + */ + boolean containsBean(Class beanType); + + /** + * Determine whether a bean of the given generics-containing type is + * already registered. + * @param beanType the generics-containing type of the bean + * @since 7.1 + */ + boolean containsBean(ParameterizedTypeReference beanType); + + + /** + * Specification for customizing a bean. + * @param the bean type + */ + interface Spec { + + /** + * Allow for instantiating this bean on a background thread. + * @see AbstractBeanDefinition#setBackgroundInit(boolean) + */ + Spec backgroundInit(); + + /** + * Set a human-readable description of this bean. + * @see BeanDefinition#setDescription(String) + */ + Spec description(String description); + + /** + * Configure this bean as a fallback autowire candidate. + * @see BeanDefinition#setFallback(boolean) + * @see #primary + */ + Spec fallback(); + + /** + * Hint that this bean has an infrastructure role, meaning it has no relevance + * to the end-user. + * @see BeanDefinition#setRole(int) + * @see BeanDefinition#ROLE_INFRASTRUCTURE + */ + Spec infrastructure(); + + /** + * Configure this bean as lazily initialized. + * @see BeanDefinition#setLazyInit(boolean) + */ + Spec lazyInit(); + + /** + * Configure this bean as not a candidate for getting autowired into another bean. + * @see BeanDefinition#setAutowireCandidate(boolean) + */ + Spec notAutowirable(); + + /** + * The sort order of this bean. This is analogous to the + * {@code @Order} annotation. + * @see AbstractBeanDefinition#ORDER_ATTRIBUTE + */ + Spec order(int order); + + /** + * Configure this bean as a primary autowire candidate. + * @see BeanDefinition#setPrimary(boolean) + * @see #fallback + */ + Spec primary(); + + /** + * Configure this bean with a prototype scope. + * @see BeanDefinition#setScope(String) + * @see BeanDefinition#SCOPE_PROTOTYPE + */ + Spec prototype(); + + /** + * Configure this bean with a custom scope. + * @since 7.0.4 + * @see BeanDefinition#setScope(String) + */ + Spec scope(String scope); + + /** + * Set the supplier to construct a bean instance. + * @see AbstractBeanDefinition#setInstanceSupplier(Supplier) + */ + Spec supplier(Function supplier); + } + + + /** + * 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 type, if any. + * @param beanClass the type the bean must match; can be an interface or superclass + * @return an instance of the single bean matching the bean type + * @see BeanFactory#getBean(String) + */ + T bean(Class beanClass) throws BeansException; + + /** + * Return the bean instance that uniquely matches the given generics-containing type, if any. + * @param beanType the generics-containing type the bean must match; can be an interface or superclass + * @return an instance of the single bean matching the bean type + * @see BeanFactory#getBean(String) + */ + T bean(ParameterizedTypeReference beanType) 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 beanClass the 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 beanClass) 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(ParameterizedTypeReference)}. + * @param beanClass the type the bean must match; can be an interface or superclass + * @return a corresponding provider handle + * @see BeanFactory#getBeanProvider(Class) + */ + ObjectProvider beanProvider(Class beanClass); + + /** + * 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 beanType the generics-containing type the bean must match; can be an interface or superclass + * @return a corresponding provider handle + * @see BeanFactory#getBeanProvider(ResolvableType) + */ + ObjectProvider beanProvider(ParameterizedTypeReference beanType); + } + +} 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..9e9a7c53d043 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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/DisposableBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/DisposableBean.java index bb7ea0abbdb0..17342f48d0bc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/DisposableBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/DisposableBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java index 97362ce1f7c9..e7d577bb01e0 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-present the original author 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/FactoryBeanNotInitializedException.java b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBeanNotInitializedException.java index 520c66b493f7..43449ebd77fc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBeanNotInitializedException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBeanNotInitializedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/HierarchicalBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/HierarchicalBeanFactory.java index d7504438bed8..4a26e34aadf3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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; /** * 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..825ff97ce3c3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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 @@ /** * 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..50a557171efe 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-present the original author 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..6e7262419de6 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-present the original author 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/NamedBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/NamedBean.java index b3ac111593ad..f458e0682754 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/NamedBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/NamedBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java b/spring-beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java index 595b40ae982a..681779464303 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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; /** * 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..4fa4c357a1de 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-present the original author 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/ObjectFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectFactory.java index 9ac3ed4ab757..0a04fab8e082 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java index a9dc61eea426..739a2d424ad6 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-present the original author 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/SmartFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java index 117f843314eb..dbbf5f613d92 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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,27 @@ package org.springframework.beans.factory; +import org.jspecify.annotations.Nullable; + /** * Extension of the {@link FactoryBean} interface. Implementations may * indicate whether they always return independent instances, for the * case where their {@link #isSingleton()} implementation returning * {@code false} does not clearly indicate independent instances. - * - *

Plain {@link FactoryBean} implementations which do not implement + * Plain {@link FactoryBean} implementations which do not implement * this extended interface are simply assumed to always return independent * instances if their {@link #isSingleton()} implementation returns * {@code false}; the exposed object is only accessed on demand. * + *

As of 7.0, this interface also allows for exposing additional object + * types for dependency injection through implementing a pair of methods: + * {@link #getObject(Class)} as well as {@link #supportsType(Class)}. + * The primary {@link #getObjectType()} will be exposed for regular access; + * only if a specific type is requested, additional types are considered. + * The container will not cache {@code SmartFactoryBean}-produced objects; + * make sure that the {@code getObject} implementation is thread-safe for + * repeated invocations. + * *

NOTE: This interface is a special purpose interface, mainly for * internal use within the framework and within collaborating frameworks. * In general, application-provided FactoryBeans should simply implement @@ -41,6 +51,42 @@ */ public interface SmartFactoryBean extends FactoryBean { + /** + * Return an instance of the given type, if supported by this factory. + *

By default, this supports the primary type exposed by the factory, as + * indicated by {@link #getObjectType()} and returned by {@link #getObject()}. + * Specific factories may support additional types for dependency injection. + * @param type the requested type + * @return a corresponding instance managed by this factory, + * or {@code null} if none available + * @throws Exception in case of creation errors + * @since 7.0 + * @see #getObject() + * @see #supportsType(Class) + */ + @SuppressWarnings("unchecked") + default @Nullable S getObject(Class type) throws Exception { + Class objectType = getObjectType(); + return (objectType != null && type.isAssignableFrom(objectType) ? (S) getObject() : null); + } + + /** + * Determine whether this factory supports the requested type. + *

By default, this supports the primary type exposed by the factory, as + * indicated by {@link #getObjectType()}. Specific factories may support + * additional types for dependency injection. + * @param type the requested type + * @return {@code true} if {@link #getObject(Class)} is able to + * return a corresponding instance, {@code false} otherwise + * @since 7.0 + * @see #getObject(Class) + * @see #getObjectType() + */ + default boolean supportsType(Class type) { + Class objectType = getObjectType(); + return (objectType != null && type.isAssignableFrom(objectType)); + } + /** * Is the object managed by this factory a prototype? That is, * will {@link #getObject()} always return an independent instance? 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..f110336481c9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ * 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 d11a5a16f823..93abbaa84cb9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.beans.factory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -32,8 +33,7 @@ @SuppressWarnings("serial") public class UnsatisfiedDependencyException extends BeanCreationException { - @Nullable - private final InjectionPoint injectionPoint; + private final @Nullable InjectionPoint injectionPoint; /** @@ -103,8 +103,7 @@ public UnsatisfiedDependencyException( * Return the injection point (field or method/constructor parameter), if known. * @since 4.3 */ - @Nullable - public InjectionPoint getInjectionPoint() { + public @Nullable InjectionPoint getInjectionPoint() { return this.injectionPoint; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.java index 7d3fc7628a5b..61815c0d1d7e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author 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.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..7bb7cf7d533f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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.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..b499538860cd 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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/Autowire.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowire.java index 27c6921dde20..3537362c8f97 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowire.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowire.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..8fe99d0ede08 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-present the original author 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. + * is applicable to all arguments. Individual parameters may be declared as + * {@link java.util.Optional}, {@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 40cf217d211e..4163e101eb75 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,11 +33,13 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; 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; @@ -62,6 +64,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; @@ -82,10 +85,8 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -102,8 +103,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 @@ -172,11 +171,9 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA private int order = Ordered.LOWEST_PRECEDENCE - 2; - @Nullable - private ConfigurableListableBeanFactory beanFactory; + private @Nullable ConfigurableListableBeanFactory beanFactory; - @Nullable - private MetadataReaderFactory metadataReaderFactory; + private @Nullable MetadataReaderFactory metadataReaderFactory; private final Set lookupMethodsChecked = ConcurrentHashMap.newKeySet(256); @@ -188,8 +185,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() { @@ -205,15 +202,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. - } } @@ -283,7 +271,7 @@ public void setBeanFactory(BeanFactory beanFactory) { "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); } this.beanFactory = clbf; - this.metadataReaderFactory = new SimpleMetadataReaderFactory(clbf.getBeanClassLoader()); + this.metadataReaderFactory = MetadataReaderFactory.create(clbf.getBeanClassLoader()); } @@ -311,8 +299,7 @@ public void resetBeanDefinition(String beanName) { } @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); String beanName = registeredBean.getBeanName(); RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); @@ -331,8 +318,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(); } @@ -360,8 +346,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); @@ -565,7 +550,7 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { } final List elements = new ArrayList<>(); - Class targetClass = clazz; + Class targetClass = ClassUtils.getUserClass(clazz); do { final List fieldElements = new ArrayList<>(); @@ -585,12 +570,11 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { final List methodElements = new ArrayList<>(); ReflectionUtils.doWithLocalMethods(targetClass, method -> { - Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); - if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { + if (method.isBridge()) { return; } - MergedAnnotation ann = findAutowiredAnnotation(bridgedMethod); - if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + MergedAnnotation ann = findAutowiredAnnotation(method); + if (ann != null && method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { if (logger.isInfoEnabled()) { logger.info("Autowired annotation is not supported on static methods: " + method); @@ -608,7 +592,7 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { } } boolean required = determineRequiredStatus(ann); - PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz); methodElements.add(new AutowiredMethodElement(method, required, pd)); } }); @@ -622,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); @@ -639,12 +622,12 @@ private MergedAnnotation findAutowiredAnnotation(AccessibleObject ao) { *

A 'required' dependency means that autowiring should fail when no beans * are found. Otherwise, the autowiring process will simply bypass the field * or method when no beans are found. - * @param ann the Autowired annotation + * @param ann a {@link MergedAnnotation} representing the Autowired annotation * @return whether the annotation indicates that a dependency is required */ protected boolean determineRequiredStatus(MergedAnnotation ann) { - return (ann.getValue(this.requiredParameterName).isEmpty() || - this.requiredParameterValue == ann.getBoolean(this.requiredParameterName)); + Optional requiredAttribute = ann.getValue(this.requiredParameterName, Boolean.class); + return (requiredAttribute.isEmpty() || this.requiredParameterValue == requiredAttribute.get()); } /** @@ -708,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); @@ -741,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); @@ -772,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); @@ -819,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); @@ -832,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); @@ -858,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++) { @@ -959,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) { @@ -984,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()); @@ -994,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)", @@ -1033,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(")"); @@ -1054,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)", @@ -1098,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 4036b74f59db..a8edce82ed27 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.util.Map; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; @@ -34,7 +36,6 @@ import org.springframework.beans.factory.support.AutowireCandidateQualifier; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -50,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 @@ -75,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 @@ -95,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); } @@ -109,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); } @@ -127,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() + @@ -146,8 +147,7 @@ else if (bf.containsBean(qualifier)) { * @return the associated qualifier value, or {@code null} if none * @since 6.2 */ - @Nullable - public static String getQualifierValue(AnnotatedElement annotatedElement) { + public static @Nullable String getQualifierValue(AnnotatedElement annotatedElement) { Qualifier qualifier = AnnotationUtils.getAnnotation(annotatedElement, Qualifier.class); return (qualifier != null ? qualifier.value() : null); } @@ -208,8 +208,8 @@ public static boolean isQualifierMatch( } } } - catch (NoSuchBeanDefinitionException ex) { - // Ignore - can't compare qualifiers for a manually registered singleton object + catch (NoSuchBeanDefinitionException ignored) { + // can't compare qualifiers for a manually registered singleton object } } return false; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Configurable.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Configurable.java index 52705b63aea0..5e24f1df2919 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Configurable.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Configurable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..63cb70e552ff 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,14 @@ import java.lang.annotation.Annotation; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -51,11 +52,9 @@ public class CustomAutowireConfigurer implements BeanFactoryPostProcessor, BeanC private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered - @Nullable - private Set customQualifierTypes; + private @Nullable Set customQualifierTypes; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); public void setOrder(int order) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java index 085fe95b0185..e63144f44681 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-2024 the original author or authors. + * Copyright 2002-present the original author 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) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java index bdd4e4d6a962..4d6e33b971d5 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,12 @@ import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; @@ -72,8 +73,7 @@ public void clear(@Nullable PropertyValues pvs) { private final Collection injectedElements; - @Nullable - private volatile Set checkedElements; + private volatile @Nullable Set checkedElements; /** @@ -198,11 +198,9 @@ public abstract static class InjectedElement { protected final boolean isField; - @Nullable - protected final PropertyDescriptor pd; + protected final @Nullable PropertyDescriptor pd; - @Nullable - protected volatile Boolean skip; + protected volatile @Nullable Boolean skip; protected InjectedElement(Member member, @Nullable PropertyDescriptor pd) { this.member = member; @@ -335,8 +333,7 @@ protected void clearPropertySkipping(@Nullable PropertyValues pvs) { /** * Either this or {@link #inject} needs to be overridden. */ - @Nullable - protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) { + protected @Nullable Object getResourceToInject(Object target, @Nullable String requestingBeanName) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java index c4e46f8ecd8a..5709c94eadcd 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-present the original author 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/Lookup.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Lookup.java index 0fca4f3316a0..337e95df0ffe 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Lookup.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Lookup.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..31635d5a5f1a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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,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; @@ -44,19 +45,20 @@ */ public final class ParameterResolutionDelegate { + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + 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 public Annotation[] getAnnotations() { - return new Annotation[0]; + return EMPTY_ANNOTATION_ARRAY; } @Override public Annotation[] getDeclaredAnnotations() { - return new Annotation[0]; + return EMPTY_ANNOTATION_ARRAY; } }; @@ -87,6 +89,31 @@ public static boolean isAutowirable(Parameter parameter, int parameterIndex) { AnnotatedElementUtils.hasAnnotation(annotatedParameter, Value.class)); } + /** + * Resolve the dependency for the supplied {@link Parameter} from the + * supplied {@link AutowireCapableBeanFactory}. + *

See {@link #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory)} + * for details. + * @param parameter the parameter whose dependency should be resolved (must not be + * {@code null}) + * @param parameterIndex the index of the parameter in the constructor or method + * that declares the parameter + * @param containingClass the concrete class that contains the parameter; this may + * differ from the class that declares the parameter in that it may be a subclass + * thereof, potentially substituting type variables (must not be {@code null}) + * @param beanFactory the {@code AutowireCapableBeanFactory} from which to resolve + * the dependency (must not be {@code null}) + * @return the resolved object, or {@code null} if none found + * @throws BeansException if dependency resolution failed + * @see #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory) + */ + public static @Nullable Object resolveDependency( + Parameter parameter, int parameterIndex, Class containingClass, AutowireCapableBeanFactory beanFactory) + throws BeansException { + + return resolveDependency(parameter, parameterIndex, null, containingClass, beanFactory); + } + /** * Resolve the dependency for the supplied {@link Parameter} from the * supplied {@link AutowireCapableBeanFactory}. @@ -99,11 +126,13 @@ public static boolean isAutowirable(Parameter parameter, int parameterIndex) { * with {@link Autowired @Autowired} with the {@link Autowired#required required} * flag set to {@code false}. *

If an explicit qualifier is not declared, the name of the parameter - * will be used as the qualifier for resolving ambiguities. + * (or a supplied custom name) will be used as the qualifier for resolving ambiguities. * @param parameter the parameter whose dependency should be resolved (must not be * {@code null}) * @param parameterIndex the index of the parameter in the constructor or method * that declares the parameter + * @param parameterName a custom name for the parameter; or {@code null} to use + * the default parameter name discovery logic * @param containingClass the concrete class that contains the parameter; this may * differ from the class that declares the parameter in that it may be a subclass * thereof, potentially substituting type variables (must not be {@code null}) @@ -111,14 +140,14 @@ public static boolean isAutowirable(Parameter parameter, int parameterIndex) { * the dependency (must not be {@code null}) * @return the resolved object, or {@code null} if none found * @throws BeansException if dependency resolution failed + * @since 7.1 * @see #isAutowirable * @see Autowired#required * @see SynthesizingMethodParameter#forExecutable(Executable, int) * @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String) */ - @Nullable - public static Object resolveDependency( - Parameter parameter, int parameterIndex, Class containingClass, AutowireCapableBeanFactory beanFactory) + public static @Nullable Object resolveDependency(Parameter parameter, int parameterIndex, + @Nullable String parameterName, Class containingClass, AutowireCapableBeanFactory beanFactory) throws BeansException { Assert.notNull(parameter, "Parameter must not be null"); @@ -131,7 +160,7 @@ public static Object resolveDependency( MethodParameter methodParameter = SynthesizingMethodParameter.forExecutable( parameter.getDeclaringExecutable(), parameterIndex); - DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required); + DependencyDescriptor descriptor = new NamedParameterDependencyDescriptor(methodParameter, required, parameterName); descriptor.setContainingClass(containingClass); return beanFactory.resolveDependency(descriptor, null); } @@ -153,7 +182,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} @@ -170,4 +199,26 @@ private static AnnotatedElement getEffectiveAnnotatedParameter(Parameter paramet return parameter; } + + @SuppressWarnings("serial") + private static class NamedParameterDependencyDescriptor extends DependencyDescriptor { + + private final @Nullable String parameterName; + + NamedParameterDependencyDescriptor(MethodParameter methodParameter, boolean required, @Nullable String parameterName) { + super(methodParameter, required); + this.parameterName = parameterName; + } + + @Override + public @Nullable String getDependencyName() { + return (this.parameterName != null ? this.parameterName : super.getDependencyName()); + } + + @Override + public boolean usesStandardBeanLookup() { + return true; + } + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Qualifier.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Qualifier.java index 3fb314f48177..7a9c1635c770 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Qualifier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Qualifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 dda7d8da513a..d18a76ffed3d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.BeanFactory; @@ -36,22 +38,23 @@ 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; /** - * {@link AutowireCandidateResolver} implementation that matches bean definition qualifiers - * against {@link Qualifier qualifier annotations} on the field or parameter to be autowired. - * Also supports suggested expression values through a {@link Value value} annotation. + * {@link AutowireCandidateResolver} implementation that matches bean definition + * qualifiers against {@link #addQualifierType(Class) qualifier annotations} on + * the field or parameter to be autowired. Also supports suggested expression + * values through a {@link #setValueAnnotationType(Class) 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 @@ -65,9 +68,9 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa /** - * 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 @Qualifier} annotation. + *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation if available. */ @SuppressWarnings("unchecked") public QualifierAnnotationAutowireCandidateResolver() { @@ -77,13 +80,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 +95,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) { @@ -105,11 +108,11 @@ public QualifierAnnotationAutowireCandidateResolver(SetThis identifies qualifier annotations for direct use (on fields, - * method parameters and constructor parameters) as well as meta - * annotations that in turn identify actual qualifier annotations. + * method parameters and constructor parameters) as well as + * meta-annotations that in turn identify actual qualifier annotations. *

This implementation only supports annotations as qualifier types. - * The default is Spring's {@link Qualifier} annotation which serves - * as a qualifier for direct use and also as a meta annotation. + * The default is Spring's {@link Qualifier @Qualifier} annotation which serves + * as a qualifier for direct use and also as a meta-annotation. * @param qualifierType the annotation type to register */ public void addQualifierType(Class qualifierType) { @@ -120,7 +123,7 @@ public void addQualifierType(Class qualifierType) { * Set the 'value' annotation type, to be used on fields, method parameters * and constructor parameters. *

The default value annotation type is the Spring-provided - * {@link Value} annotation. + * {@link Value @Value} annotation. *

This setter property exists so that developers can provide their own * (non-Spring-specific) annotation type to indicate a default value * expression for a specific argument. @@ -144,31 +147,41 @@ 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) { + 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; + } boolean checkMeta = true; boolean fallbackToMeta = false; if (isQualifier(type)) { @@ -184,6 +197,9 @@ protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] an boolean foundMeta = false; for (Annotation metaAnn : type.getAnnotations()) { Class metaType = metaAnn.annotationType(); + if (isPlainJavaAnnotation(metaType)) { + continue; + } if (isQualifier(metaType)) { qualifierFound = true; foundMeta = true; @@ -201,11 +217,21 @@ protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] an } } } - return (qualifierFound || ((RootBeanDefinition) bdHolder.getBeanDefinition()).isDefaultCandidate()); + 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) { @@ -265,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; @@ -301,14 +327,12 @@ protected boolean checkQualifier( return true; } - @Nullable - protected Annotation getQualifiedElementAnnotation(RootBeanDefinition bd, Class type) { + protected @Nullable Annotation getQualifiedElementAnnotation(RootBeanDefinition bd, Class type) { AnnotatedElement qualifiedElement = bd.getQualifiedElement(); return (qualifiedElement != null ? AnnotationUtils.getAnnotation(qualifiedElement, type) : null); } - @Nullable - protected Annotation getFactoryMethodAnnotation(RootBeanDefinition bd, Class type) { + protected @Nullable Annotation getFactoryMethodAnnotation(RootBeanDefinition bd, Class type) { Method resolvedFactoryMethod = bd.getResolvedFactoryMethod(); return (resolvedFactoryMethod != null ? AnnotationUtils.getAnnotation(resolvedFactoryMethod, type) : null); } @@ -324,8 +348,20 @@ public boolean isRequired(DependencyDescriptor descriptor) { if (!super.isRequired(descriptor)) { return false; } - Autowired autowired = descriptor.getAnnotation(Autowired.class); - return (autowired == null || autowired.required()); + + for (Annotation ann : descriptor.getAnnotations()) { + // Directly present? + if (ann instanceof Autowired autowired) { + return autowired.required(); + } + // Meta-present? + Autowired autowired = AnnotationUtils.findAnnotation(ann.annotationType(), Autowired.class); + if (autowired != null) { + return autowired.required(); + } + } + // No @Autowired annotation present: default to true. + return true; } /** @@ -340,12 +376,22 @@ public boolean hasQualifier(DependencyDescriptor descriptor) { return true; } } + MethodParameter methodParam = descriptor.getMethodParameter(); + if (methodParam != null) { + Method method = methodParam.getMethod(); + if (method == null || void.class == method.getReturnType()) { + for (Annotation annotation : methodParam.getMethodAnnotations()) { + if (isQualifier(annotation.annotationType())) { + return true; + } + } + } + } return false; } @Override - @Nullable - public String getSuggestedName(DependencyDescriptor descriptor) { + public @Nullable String getSuggestedName(DependencyDescriptor descriptor) { for (Annotation annotation : descriptor.getAnnotations()) { if (isQualifier(annotation.annotationType())) { Object value = AnnotationUtils.getValue(annotation); @@ -362,8 +408,7 @@ public String getSuggestedName(DependencyDescriptor descriptor) { * @see Value */ @Override - @Nullable - public Object getSuggestedValue(DependencyDescriptor descriptor) { + public @Nullable Object getSuggestedValue(DependencyDescriptor descriptor) { Object value = findValue(descriptor.getAnnotations()); if (value == null) { MethodParameter methodParam = descriptor.getMethodParameter(); @@ -377,8 +422,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/Value.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Value.java index bfa27305a6a1..a8dfc6c7dcf4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Value.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Value.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..16267041b9fc --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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..1e7db491da7c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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..5b2a4a62c167 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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..592badd663aa 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-present the original author 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..4618f11b9b45 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-present the original author 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,12 +41,11 @@ 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 '" + - ClassUtils.getQualifiedName(requiredType) + "' for value [" + value + "]"); + requiredType.getTypeName() + "' for value [" + value + "]"); } return (T) value; } @@ -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..cb04f1f0b6b8 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-present the original author 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); + code.add("$L.get($L)", variableName, i); } else { - code.add("$L.get($L, $T.class)", variableName, i - startIndex, - parameterTypes[i]); + code.add("$L.get($L, $T.class)", variableName, i, parameterTypes[i]); } } return code.build(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredElementResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredElementResolver.java index 9fdfce349d38..9096f15dcd1a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredElementResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredElementResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 1c5f68fe902f..2b36fc9d396d 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-present the original author 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(); 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 e902bee884bc..2f138ad15204 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ import java.util.Set; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.ExecutableMode; import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; @@ -31,7 +33,6 @@ import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; @@ -62,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 = 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 074e87df871b..23cab1f3db13 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.lang.model.element.Modifier; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GeneratedClass; import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GeneratedMethods; @@ -28,7 +30,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.javapoet.ClassName; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -46,8 +47,7 @@ class BeanDefinitionMethodGenerator { private final RegisteredBean registeredBean; - @Nullable - private final String currentPropertyName; + private final @Nullable String currentPropertyName; private final List aotContributions; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactory.java index 580a0533cedb..fb25f4bf2d11 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,12 @@ import 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 f183d2b05e14..1310230d94cf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,10 +33,11 @@ import java.util.function.Function; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.ValueCodeGenerator; import org.springframework.aot.generate.ValueCodeGenerator.Delegate; -import org.springframework.aot.generate.ValueCodeGeneratorDelegates; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -52,10 +53,12 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.AutowireCandidateQualifier; import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.LookupOverride; +import org.springframework.beans.factory.support.MethodOverride; +import org.springframework.beans.factory.support.ReplaceOverride; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -102,44 +105,55 @@ class BeanDefinitionPropertiesCodeGenerator { this.hints = hints; this.attributeFilter = attributeFilter; - List allDelegates = new ArrayList<>(); - allDelegates.add((valueCodeGenerator, value) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), value)); - allDelegates.addAll(additionalDelegates); - allDelegates.addAll(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES); - allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES); - this.valueCodeGenerator = ValueCodeGenerator.with(allDelegates).scoped(generatedMethods); + List customDelegates = new ArrayList<>(); + customDelegates.add((valueCodeGenerator, value) -> + customValueCodeGenerator.apply(PropertyNamesStack.peek(), value)); + customDelegates.addAll(additionalDelegates); + this.valueCodeGenerator = BeanDefinitionPropertyValueCodeGeneratorDelegates + .createValueCodeGenerator(generatedMethods, customDelegates); } - 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); addQualifiers(code, beanDefinition); + addMethodOverrides(code, beanDefinition); return code.build(); } 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)) { @@ -174,9 +188,9 @@ private void addInitDestroyHint(Class beanUserClass, String methodName) { Method method = ReflectionUtils.findMethod(methodDeclaringClass, methodName); if (method != null) { this.hints.reflection().registerMethod(method, ExecutableMode.INVOKE); - Method interfaceMethod = ClassUtils.getInterfaceMethodIfPossible(method, beanUserClass); - if (!interfaceMethod.equals(method)) { - this.hints.reflection().registerMethod(interfaceMethod, ExecutableMode.INVOKE); + Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(method, beanUserClass); + if (!publiclyAccessibleMethod.equals(method)) { + this.hints.reflection().registerMethod(publiclyAccessibleMethod, ExecutableMode.INVOKE); } } } @@ -208,7 +222,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)", @@ -222,7 +235,8 @@ private void addPropertyValues(CodeBlock.Builder code, RootBeanDefinition beanDe MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); if (!propertyValues.isEmpty()) { Class infrastructureType = getInfrastructureType(beanDefinition); - Map writeMethods = (infrastructureType != Object.class) ? getWriteMethods(infrastructureType) : Collections.emptyMap(); + Map writeMethods = (infrastructureType != Object.class ? + getWriteMethods(infrastructureType) : Collections.emptyMap()); for (PropertyValue propertyValue : propertyValues) { String name = propertyValue.getName(); CodeBlock valueCode = generateValue(name, propertyValue.getValue()); @@ -241,10 +255,10 @@ private void registerReflectionHints(RootBeanDefinition beanDefinition, Method w // ReflectionUtils#findField searches recursively in the type hierarchy Class searchType = beanDefinition.getTargetType(); while (searchType != null && searchType != writeMethod.getDeclaringClass()) { - this.hints.reflection().registerType(searchType, MemberCategory.DECLARED_FIELDS); + this.hints.reflection().registerType(searchType, MemberCategory.ACCESS_DECLARED_FIELDS); searchType = searchType.getSuperclass(); } - this.hints.reflection().registerType(writeMethod.getDeclaringClass(), MemberCategory.DECLARED_FIELDS); + this.hints.reflection().registerType(writeMethod.getDeclaringClass(), MemberCategory.ACCESS_DECLARED_FIELDS); } private void addQualifiers(CodeBlock.Builder code, RootBeanDefinition beanDefinition) { @@ -263,9 +277,39 @@ private void addQualifiers(CodeBlock.Builder code, RootBeanDefinition beanDefini } } + private void addMethodOverrides(CodeBlock.Builder code, RootBeanDefinition beanDefinition) { + if (beanDefinition.hasMethodOverrides()) { + for (MethodOverride methodOverride : beanDefinition.getMethodOverrides().getOverrides()) { + if (methodOverride instanceof LookupOverride lookupOverride) { + Collection arguments = new ArrayList<>(); + arguments.add(CodeBlock.of("$S", lookupOverride.getMethodName())); + arguments.add(CodeBlock.of("$S", lookupOverride.getBeanName())); + code.addStatement("$L.getMethodOverrides().addOverride(new $T($L))", BEAN_DEFINITION_VARIABLE, + LookupOverride.class, CodeBlock.join(arguments, ", ")); + } + else if (methodOverride instanceof ReplaceOverride replaceOverride) { + Collection arguments = new ArrayList<>(); + arguments.add(CodeBlock.of("$S", replaceOverride.getMethodName())); + arguments.add(CodeBlock.of("$S", replaceOverride.getMethodReplacerBeanName())); + List typeIdentifiers = replaceOverride.getTypeIdentifiers(); + if (!typeIdentifiers.isEmpty()) { + arguments.add(CodeBlock.of("java.util.List.of($S)", + StringUtils.collectionToDelimitedString(typeIdentifiers, ", "))); + } + code.addStatement("$L.getMethodOverrides().addOverride(new $T($L))", BEAN_DEFINITION_VARIABLE, + ReplaceOverride.class, CodeBlock.join(arguments, ", ")); + } + else { + throw new UnsupportedOperationException("Unexpected MethodOverride subclass: " + + methodOverride.getClass().getName()); + } + } + } + } + private CodeBlock generateValue(@Nullable String name, @Nullable Object value) { + PropertyNamesStack.push(name); try { - PropertyNamesStack.push(name); return this.valueCodeGenerator.generateCode(value); } finally { @@ -306,8 +350,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) { @@ -333,8 +376,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); @@ -342,16 +384,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); @@ -361,9 +402,8 @@ 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 @@ -388,8 +428,7 @@ static void pop() { threadLocal.get().pop(); } - @Nullable - static String peek() { + static @Nullable String peek() { String value = threadLocal.get().peek(); return ("".equals(value) ? null : value); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java index 1b9f1fcc8ad5..30b38e496c11 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.beans.factory.aot; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.ValueCodeGenerator; @@ -29,6 +32,7 @@ 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.AutowiredPropertyMarker; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.TypedStringValue; @@ -37,7 +41,6 @@ import org.springframework.beans.factory.support.ManagedSet; import org.springframework.javapoet.AnnotationSpec; import org.springframework.javapoet.CodeBlock; -import org.springframework.lang.Nullable; /** * Code generator {@link Delegate} for common bean definition property values. @@ -45,7 +48,7 @@ * @author Stephane Nicoll * @since 6.1.2 */ -abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { +public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { /** * A list of {@link Delegate} implementations for the following common bean @@ -57,6 +60,7 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { *

  • {@link LinkedHashMap}
  • *
  • {@link BeanReference}
  • *
  • {@link TypedStringValue}
  • + *
  • {@link AutowiredPropertyMarker}
  • * * When combined with {@linkplain ValueCodeGeneratorDelegates#INSTANCES the * delegates for common value types}, this should be added first as they have @@ -68,10 +72,31 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { new ManagedMapDelegate(), new LinkedHashMapDelegate(), new BeanReferenceDelegate(), - new TypedStringValueDelegate() + new TypedStringValueDelegate(), + new AutowiredPropertyMarkerDelegate() ); + /** + * Create a {@link ValueCodeGenerator} instance with both these + * {@link #INSTANCES delegate} and the {@link ValueCodeGeneratorDelegates#INSTANCES + * core delegates}. + * @param generatedMethods the {@link GeneratedMethods} to use + * @param customDelegates additional delegates that should be considered first + * @return a configured value code generator + * @since 7.0 + * @see ValueCodeGenerator#add(List) + */ + public static ValueCodeGenerator createValueCodeGenerator( + GeneratedMethods generatedMethods, List customDelegates) { + List allDelegates = new ArrayList<>(); + allDelegates.addAll(customDelegates); + allDelegates.addAll(INSTANCES); + allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES); + return ValueCodeGenerator.with(allDelegates).scoped(generatedMethods); + } + + /** * {@link Delegate} for {@link ManagedList} types. */ @@ -102,8 +127,7 @@ private static class ManagedMapDelegate implements Delegate { private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class); @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { if (value instanceof ManagedMap managedMap) { return generateManagedMapCode(valueCodeGenerator, managedMap); } @@ -139,8 +163,7 @@ private CodeBlock generateManagedMapCode(ValueCodeGenerator valueCodeGene private static class LinkedHashMapDelegate extends MapDelegate { @Override - @Nullable - protected CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map map) { + protected @Nullable CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map map) { GeneratedMethods generatedMethods = valueCodeGenerator.getGeneratedMethods(); if (map instanceof LinkedHashMap && generatedMethods != null) { return generateLinkedHashMapCode(valueCodeGenerator, generatedMethods, map); @@ -156,6 +179,8 @@ private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerato .builder(SuppressWarnings.class) .addMember("value", "{\"rawtypes\", \"unchecked\"}") .build()); + method.addModifiers(javax.lang.model.element.Modifier.PRIVATE, + javax.lang.model.element.Modifier.STATIC); method.returns(Map.class); method.addStatement("$T map = new $T($L)", Map.class, LinkedHashMap.class, map.size()); @@ -175,12 +200,11 @@ private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerato private static class BeanReferenceDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { if (value instanceof RuntimeBeanReference runtimeBeanReference && runtimeBeanReference.getBeanType() != null) { - return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class, - runtimeBeanReference.getBeanType()); + return CodeBlock.of("new $T($S, $T.class)", RuntimeBeanReference.class, + runtimeBeanReference.getBeanName(), runtimeBeanReference.getBeanType()); } else if (value instanceof BeanReference beanReference) { return CodeBlock.of("new $T($S)", RuntimeBeanReference.class, @@ -197,8 +221,7 @@ else if (value instanceof BeanReference beanReference) { private static class TypedStringValueDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { if (value instanceof TypedStringValue typedStringValue) { return generateTypeStringValueCode(valueCodeGenerator, typedStringValue); } @@ -214,4 +237,19 @@ private CodeBlock generateTypeStringValueCode(ValueCodeGenerator valueCodeGenera return valueCodeGenerator.generateCode(value); } } + + /** + * {@link Delegate} for {@link AutowiredPropertyMarker} types. + */ + private static class AutowiredPropertyMarkerDelegate implements Delegate { + + @Override + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof AutowiredPropertyMarker) { + return CodeBlock.of("$T.INSTANCE", AutowiredPropertyMarker.class); + } + return null; + } + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution.java index 41e96bd8cf93..905a94d710c0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..398b5dfee608 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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/BeanFactoryInitializationCode.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationCode.java index e7da3299e81a..de5a608362f6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationCode.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationCode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.MethodReference; +import org.springframework.javapoet.ClassName; /** * Interface that can be used to configure the code that will be generated to @@ -25,6 +26,7 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @author Sebastien Deleuze * @since 6.0 * @see BeanFactoryInitializationAotContribution */ @@ -41,6 +43,13 @@ public interface BeanFactoryInitializationCode { */ GeneratedMethods getMethods(); + /** + * Return the name of the class used by the initializing code. + * @return the generated class name + * @since 7.0.2 + */ + ClassName getClassName(); + /** * Add an initializer method call. An initializer can use a flexible signature, * using any of the following: 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 11edc1dd9a35..48a1fcca45ce 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,29 +27,28 @@ 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; import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionValueResolver; +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.SimpleInstantiationStrategy; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.function.ThrowingBiFunction; import org.springframework.util.function.ThrowingFunction; @@ -79,6 +78,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 +87,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 +116,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 +137,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 +153,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 +167,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)); - } - return invokeBeanSupplier(executable, - () -> instantiate(registeredBean.getBeanFactory(), executable, arguments.toArray())); - } - - private T invokeBeanSupplier(Executable executable, ThrowingSupplier beanSupplier) { - if (!(executable instanceof Method method)) { - return beanSupplier.get(); + if (this.generatorWithoutArguments != null) { + Executable executable = getFactoryMethodForGenerator(); + return invokeBeanSupplier(executable, () -> this.generatorWithoutArguments.apply(registeredBean)); } - try { - SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(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)); } - 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 +242,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); @@ -327,8 +325,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 +342,35 @@ 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); + if (registeredBean.getBeanFactory() instanceof DefaultListableBeanFactory dlbf && + registeredBean.getMergedBeanDefinition().hasMethodOverrides()) { + return dlbf.getInstantiationStrategy().instantiate(registeredBean.getMergedBeanDefinition(), + registeredBean.getBeanName(), registeredBean.getBeanFactory()); } + 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 +399,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 +427,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 +441,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..fc76f514d109 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,9 @@ import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -40,8 +41,7 @@ public interface BeanRegistrationAotContribution { * default code generation isn't suitable. * @param generationContext the generation context * @param codeFragments the existing code fragments - * @return the code fragments to use, may be the original instance or a - * wrapper + * @return the code fragments to use, may be the original instance or a wrapper */ default BeanRegistrationCodeFragments customizeBeanRegistrationCodeFragments( GenerationContext generationContext, BeanRegistrationCodeFragments codeFragments) { @@ -77,8 +77,7 @@ public BeanRegistrationCodeFragments customizeBeanRegistrationCodeFragments( return defaultCodeFragments.apply(codeFragments); } @Override - public void applyTo(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode) { + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { } }; } @@ -94,8 +93,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..4d135c23062e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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/BeanRegistrationCode.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCode.java index a55a6efa4245..90361c65031c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCode.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragments.java index db1bd2e81556..cc7dc90d8e40 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragments.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragments.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,14 +31,15 @@ /** * Generate the various fragments of code needed to register a bean. - *

    - * A default implementation is provided that suits most needs and custom code + * + *

    A default implementation is provided that suits most needs and custom code * fragments are only expected to be used by library authors having built custom * arrangement on top of the core container. - *

    - * Users are not expected to implement this interface directly, but rather extends - * from {@link BeanRegistrationCodeFragmentsDecorator} and only override the - * necessary method(s). + * + *

    Users are not expected to implement this interface directly, but rather + * extends from {@link BeanRegistrationCodeFragmentsDecorator} and only override + * the necessary method(s). + * * @author Phillip Webb * @author Stephane Nicoll * @since 6.0 @@ -48,12 +49,12 @@ public interface BeanRegistrationCodeFragments { /** - * The variable name to used when creating the bean definition. + * The variable name used when creating the bean definition. */ String BEAN_DEFINITION_VARIABLE = "beanDefinition"; /** - * The variable name to used when creating the bean definition. + * The variable name used when creating the bean definition. */ String INSTANCE_SUPPLIER_VARIABLE = "instanceSupplier"; @@ -69,8 +70,7 @@ public interface BeanRegistrationCodeFragments { /** * Generate the code that defines the new bean definition instance. - *

    - * This should declare a variable named {@value BEAN_DEFINITION_VARIABLE} + *

    This should declare a variable named {@value BEAN_DEFINITION_VARIABLE} * so that further fragments can refer to the variable to further tune * the bean definition. * @param generationContext the generation context @@ -94,14 +94,13 @@ CodeBlock generateSetBeanDefinitionPropertiesCode( /** * Generate the code that sets the instance supplier on the bean definition. - *

    - * The {@code postProcessors} represent methods to be exposed once the + *

    The {@code postProcessors} represent methods to be exposed once the * instance has been created to further configure it. Each method should * accept two parameters, the {@link RegisteredBean} and the bean * instance, and should return the modified bean instance. * @param generationContext the generation context * @param beanRegistrationCode the bean registration code - * @param instanceSupplierCode the instance supplier code supplier code + * @param instanceSupplierCode the instance supplier code * @param postProcessors any instance post processors that should be applied * @return the generated code * @see #generateInstanceSupplierCode 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..37e8c50e0040 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-present the original author 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/BeanRegistrationCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeGenerator.java index 98564d4852e7..5ef293f128b7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,6 +58,7 @@ class BeanRegistrationCodeGenerator implements BeanRegistrationCode { this.codeFragments = codeFragments; } + @Override public ClassName getClassName() { return this.className; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationExcludeFilter.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationExcludeFilter.java index 10812417bc9d..294942f1979f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationExcludeFilter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationExcludeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 4c91aa49442f..077e61fe35da 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 java.util.Map; +import java.util.List; +import java.util.function.BiConsumer; import javax.lang.model.element.Modifier; @@ -26,12 +27,14 @@ 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; /** @@ -50,10 +53,17 @@ class BeanRegistrationsAotContribution private static final String BEAN_FACTORY_PARAMETER_NAME = "beanFactory"; - private final Map registrations; + private static final int MAX_REGISTRATIONS_PER_FILE = 5000; + private static final int MAX_REGISTRATIONS_PER_METHOD = 1000; - BeanRegistrationsAotContribution(Map registrations) { + private static final ArgumentCodeGenerator argumentCodeGenerator = ArgumentCodeGenerator + .of(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME); + + private final List registrations; + + + BeanRegistrationsAotContribution(List registrations) { this.registrations = registrations; } @@ -62,14 +72,10 @@ class BeanRegistrationsAotContribution public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { - GeneratedClass generatedClass = generationContext.getGeneratedClasses() - .addForFeature("BeanFactoryRegistrations", type -> { - type.addJavadoc("Register bean definitions for the bean factory."); - type.addModifiers(Modifier.PUBLIC); - }); + GeneratedClass generatedClass = createBeanFactoryRegistrationClass(generationContext); BeanRegistrationsCodeGenerator codeGenerator = new BeanRegistrationsCodeGenerator(generatedClass); - GeneratedMethod generatedBeanDefinitionsMethod = codeGenerator.getMethods().add("registerBeanDefinitions", method -> - generateRegisterBeanDefinitionsMethod(method, generationContext, codeGenerator)); + GeneratedMethod generatedBeanDefinitionsMethod = generateBeanRegistrationCode(generationContext, + generatedClass, codeGenerator); beanFactoryInitializationCode.addInitializer(generatedBeanDefinitionsMethod.toMethodReference()); GeneratedMethod generatedAliasesMethod = codeGenerator.getMethods().add("registerAliases", this::generateRegisterAliasesMethod); @@ -77,22 +83,46 @@ public void applyTo(GenerationContext generationContext, generateRegisterHints(generationContext.getRuntimeHints(), this.registrations); } - private void generateRegisterBeanDefinitionsMethod(MethodSpec.Builder method, - GenerationContext generationContext, BeanRegistrationsCode beanRegistrationsCode) { + private GeneratedMethod generateBeanRegistrationCode(GenerationContext generationContext, GeneratedClass mainGeneratedClass, BeanRegistrationsCodeGenerator mainCodeGenerator) { + if (this.registrations.size() < MAX_REGISTRATIONS_PER_FILE) { + return generateBeanRegistrationClass(generationContext, mainCodeGenerator, 0, this.registrations.size()); + } + else { + return mainGeneratedClass.getMethods().add("registerBeanDefinitions", method -> { + method.addJavadoc("Register the bean definitions."); + method.addModifiers(Modifier.PUBLIC); + method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME); + CodeBlock.Builder body = CodeBlock.builder(); + Registration.doWithSlice(this.registrations, MAX_REGISTRATIONS_PER_FILE, (start, end) -> { + GeneratedClass sliceGeneratedClass = createBeanFactoryRegistrationClass(generationContext); + BeanRegistrationsCodeGenerator sliceCodeGenerator = new BeanRegistrationsCodeGenerator(sliceGeneratedClass); + GeneratedMethod generatedMethod = generateBeanRegistrationClass(generationContext, sliceCodeGenerator, start, end); + body.addStatement(generatedMethod.toMethodReference().toInvokeCodeBlock(argumentCodeGenerator)); + }); + method.addCode(body.build()); + }); + } + } - 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); + private GeneratedMethod generateBeanRegistrationClass(GenerationContext generationContext, + BeanRegistrationsCodeGenerator codeGenerator, int start, int end) { + + return codeGenerator.getMethods().add("registerBeanDefinitions", method -> { + method.addJavadoc("Register the bean definitions."); + method.addModifiers(Modifier.PUBLIC); + method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME); + List sliceRegistrations = this.registrations.subList(start, end); + new BeanDefinitionsRegistrationGenerator( + generationContext, codeGenerator, sliceRegistrations, start).generateBeanRegistrationsCode(method); }); - method.addCode(code.build()); + } + + private static GeneratedClass createBeanFactoryRegistrationClass(GenerationContext generationContext) { + return generationContext.getGeneratedClasses() + .addForFeature("BeanFactoryRegistrations", type -> { + type.addJavadoc("Register bean definitions for the bean factory."); + type.addModifiers(Modifier.PUBLIC); + }); } private void generateRegisterAliasesMethod(MethodSpec.Builder method) { @@ -100,30 +130,59 @@ private void generateRegisterAliasesMethod(MethodSpec.Builder method) { 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); - hints.registerForInterfaces(beanClass, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + Class beanClass = registration.registeredBean.getBeanClass(); + hints.registerType(beanClass); + hints.registerForInterfaces(beanClass, TypeHint.Builder::withMembers); }); } /** * 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(); + } + + /** + * Invoke an action for each slice of the given {@code registrations}. The + * {@code action} is invoked for each slice with the start and end index of the + * given list of registrations. Elements to process can be retrieved using + * {@link List#subList(int, int)}. + * @param registrations the registrations to process + * @param sliceSize the size of a slice + * @param action the action to invoke for each slice + */ + static void doWithSlice(List registrations, int sliceSize, + BiConsumer action) { + + int index = 0; + int end = 0; + while (end < registrations.size()) { + int start = index * sliceSize; + end = Math.min(start + sliceSize, registrations.size()); + action.accept(start, end); + index++; + } + } + + } /** @@ -150,4 +209,87 @@ public GeneratedMethods getMethods() { } + /** + * Generate code for bean registrations. Limited to {@value #MAX_REGISTRATIONS_PER_METHOD} + * beans per method to avoid hitting a limit. + */ + static final class BeanDefinitionsRegistrationGenerator { + + private final GenerationContext generationContext; + + private final BeanRegistrationsCodeGenerator codeGenerator; + + private final List registrations; + + private final int globalStart; + + + BeanDefinitionsRegistrationGenerator(GenerationContext generationContext, + BeanRegistrationsCodeGenerator codeGenerator, List registrations, int globalStart) { + + this.generationContext = generationContext; + this.codeGenerator = codeGenerator; + this.registrations = registrations; + this.globalStart = globalStart; + } + + void generateBeanRegistrationsCode(MethodSpec.Builder method) { + 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"); + Registration.doWithSlice(this.registrations, MAX_REGISTRATIONS_PER_METHOD, + (start, end) -> { + GeneratedMethod sliceMethod = generateSliceMethod(start, end); + code.addStatement(sliceMethod.toMethodReference().toInvokeCodeBlock( + argumentCodeGenerator, this.codeGenerator.getClassName())); + }); + method.addCode(code.build()); + } + } + + private GeneratedMethod generateSliceMethod(int start, int end) { + String description = "Register the bean definitions from %s to %s." + .formatted(this.globalStart + start, this.globalStart + 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..a1bb8aed1cc4 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-present the original author 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/BeanRegistrationsCode.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsCode.java index f325d7da0c67..e9c09a17bdf2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsCode.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsCode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 93676d678bd9..91479f40e2a2 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,18 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +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; /** @@ -37,7 +42,7 @@ * @since 6.1 * @see SuppressWarnings */ -class CodeWarnings { +public class CodeWarnings { private final Set warnings = new LinkedHashSet<>(); @@ -58,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; } @@ -78,6 +83,7 @@ public CodeWarnings detectDeprecation(Stream elements) { * 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)) { @@ -98,10 +104,31 @@ public CodeWarnings detectDeprecation(ResolvableType resolvableType) { * @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()); } /** @@ -112,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()) { @@ -123,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()); 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 50564ee23a33..e11473e29719 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.AccessControl; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; @@ -40,14 +42,12 @@ 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 @@ -65,8 +65,8 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme private final Supplier instantiationDescriptor; - DefaultBeanRegistrationCodeFragments(BeanRegistrationsCode beanRegistrationsCode, - RegisteredBean registeredBean, + DefaultBeanRegistrationCodeFragments( + BeanRegistrationsCode beanRegistrationsCode, RegisteredBean registeredBean, BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory) { this.beanRegistrationsCode = beanRegistrationsCode; @@ -79,9 +79,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme @Override public ClassName getTarget(RegisteredBean registeredBean) { if (hasInstanceSupplier()) { - String resourceDescription = registeredBean.getMergedBeanDefinition().getResourceDescription(); - throw new IllegalStateException("Error processing bean with name '" + registeredBean.getBeanName() + "'" + - (resourceDescription != null ? " defined in " + resourceDescription : "") + ": instance supplier is not supported"); + throw new AotBeanProcessingException(registeredBean, "instance supplier is not supported"); } Class target = extractDeclaringClass(registeredBean, this.instantiationDescriptor.get()); while (target.getName().startsWith("java.") && registeredBean.isInnerBean()) { @@ -94,9 +92,8 @@ public ClassName getTarget(RegisteredBean registeredBean) { private Class extractDeclaringClass(RegisteredBean registeredBean, InstantiationDescriptor instantiationDescriptor) { Class declaringClass = ClassUtils.getUserClass(instantiationDescriptor.targetClass()); - if (instantiationDescriptor.executable() instanceof Constructor - && AccessControl.forMember(instantiationDescriptor.executable()).isPublic() - && FactoryBean.class.isAssignableFrom(declaringClass)) { + if (instantiationDescriptor.executable() instanceof Constructor ctor && + AccessControl.forMember(ctor).isPublic() && FactoryBean.class.isAssignableFrom(declaringClass)) { return extractTargetClassFromFactoryBean(declaringClass, registeredBean.getBeanType()); } return declaringClass; @@ -105,8 +102,7 @@ private Class extractDeclaringClass(RegisteredBean registeredBean, Instantiat /** * 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 @@ -127,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(); } @@ -162,30 +156,28 @@ private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class 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(), - additionalDelegates, (name, value) -> generateValueCode(generationContext, name, value) - ).generateCode(beanDefinition); - } - @Nullable - protected CodeBlock generateValueCode(GenerationContext generationContext, - String name, Object value) { + return new BeanDefinitionPropertiesCodeGenerator( + generationContext.getRuntimeHints(), attributeFilter, + beanRegistrationCode.getMethods(), additionalDelegates, + (name, value) -> generateValueCode(generationContext, name, value)) + .generateCode(beanDefinition); + } + protected @Nullable CodeBlock generateValueCode(GenerationContext generationContext, String name, Object value) { RegisteredBean innerRegisteredBean = getInnerRegisteredBean(value); if (innerRegisteredBean != null) { BeanDefinitionMethodGenerator methodGenerator = this.beanDefinitionMethodGeneratorFactory @@ -198,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); } @@ -211,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()) { @@ -233,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.instantiationDescriptor.get()); + return new InstanceSupplierCodeGenerator(generationContext, + beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut) + .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); @@ -254,7 +245,7 @@ public CodeBlock generateReturnCode(GenerationContext generationContext, } private boolean hasInstanceSupplier() { - return this.registeredBean.getMergedBeanDefinition().getInstanceSupplier() != null; + return (this.registeredBean.getMergedBeanDefinition().getInstanceSupplier() != null); } } 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 c65ab4ed1f7e..1c295379f20f 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; @@ -67,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 @@ -83,12 +83,13 @@ 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(""); + private static final boolean KOTLIN_REFLECT_PRESENT = KotlinDetector.isKotlinReflectPresent(); + private final GenerationContext generationContext; @@ -100,7 +101,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 @@ -116,6 +117,7 @@ public InstanceSupplierCodeGenerator(GenerationContext generationContext, this.allowDirectSupplierShortcut = allowDirectSupplierShortcut; } + /** * Generate the instance supplier code. * @param registeredBean the bean to handle @@ -145,162 +147,146 @@ public CodeBlock generateCode(RegisteredBean registeredBean, InstantiationDescri 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 (KOTLIN_REFLECT_PRESENT && 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()) || + registeredBean.getMergedBeanDefinition().hasMethodOverrides()) { + 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) { + private CodeBlock generateCodeForAccessibleConstructor(ConstructorDescriptor descriptor) { + Constructor constructor = descriptor.constructor(); + this.generationContext.getRuntimeHints().reflection().registerType(constructor.getDeclaringClass()); - this.generationContext.getRuntimeHints().reflection().registerConstructor( - constructor, ExecutableMode.INTROSPECT); - - 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, Class targetClass) { - String beanName = registeredBean.getBeanName(); - Class targetClassToUse = ClassUtils.getUserClass(targetClass); - 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, targetClassToUse, dependsOnBean); + if (!isVisible(factoryMethod, targetClass)) { + return generateCodeForInaccessibleFactoryMethod(registeredBean.getBeanName(), factoryMethod, targetClass); } - return generateCodeForInaccessibleFactoryMethod(beanName, factoryMethod, targetClassToUse); + return generateCodeForAccessibleFactoryMethod(registeredBean.getBeanName(), factoryMethod, targetClass, + registeredBean.getMergedBeanDefinition().getFactoryBeanName()); } private CodeBlock generateCodeForAccessibleFactoryMethod(String beanName, - Method factoryMethod, Class targetClass, 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, targetClass, factoryMethod.getName()); code.add(".withGenerator(($L) -> $T.$L())", REGISTERED_BEAN_PARAMETER_NAME, - targetClass, factoryMethod.getName()); + ClassUtils.getUserClass(targetClass), factoryMethod.getName()); return code.build(); } GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> buildGetInstanceMethodForFactoryMethod(method, beanName, factoryMethod, - targetClass, dependsOnBean, PRIVATE_STATIC)); + targetClass, factoryBeanName, PRIVATE_STATIC)); return generateReturnStatement(getInstanceMethod); } @@ -309,9 +295,12 @@ private CodeBlock generateCodeForInaccessibleFactoryMethod( this.generationContext.getRuntimeHints().reflection().registerMethod(factoryMethod, ExecutableMode.INVOKE); GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> { + CodeWarnings codeWarnings = new CodeWarnings(); Class suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType()); + codeWarnings.detectDeprecation(suppliedType, factoryMethod); method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); method.addModifiers(PRIVATE_STATIC); + codeWarnings.suppress(method); method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, suppliedType)); method.addStatement(generateInstanceSupplierForFactoryMethod( factoryMethod, suppliedType, targetClass, factoryMethod.getName())); @@ -321,12 +310,12 @@ private CodeBlock generateCodeForInaccessibleFactoryMethod( private void buildGetInstanceMethodForFactoryMethod(MethodSpec.Builder method, String beanName, Method factoryMethod, Class targetClass, - boolean dependsOnBean, javax.lang.model.element.Modifier... modifiers) { + @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(targetClass, 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); @@ -340,12 +329,11 @@ private void buildGetInstanceMethodForFactoryMethod(MethodSpec.Builder method, boolean hasArguments = factoryMethod.getParameterCount() > 0; CodeBlock arguments = hasArguments ? - new AutowiredArgumentsCodeGenerator(targetClass, factoryMethod) - .generateCode(factoryMethod.getParameterTypes()) - : NO_ARGS; + new AutowiredArgumentsCodeGenerator(ClassUtils.getUserClass(targetClass), factoryMethod) + .generateCode(factoryMethod.getParameterTypes()) : NO_ARGS; CodeBlock newInstance = generateNewInstanceCodeForMethod( - dependsOnBean, targetClass, factoryMethodName, arguments); + factoryBeanName, ClassUtils.getUserClass(targetClass), factoryMethodName, arguments); code.add(generateWithGeneratorCode(hasArguments, newInstance)); method.addStatement(code.build()); } @@ -358,19 +346,19 @@ private CodeBlock generateInstanceSupplierForFactoryMethod(Method factoryMethod, 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, targetClass, factoryMethodName, parameterTypes); } - private CodeBlock generateNewInstanceCodeForMethod(boolean dependsOnBean, + private CodeBlock generateNewInstanceCodeForMethod(@Nullable String factoryBeanName, Class targetClass, String factoryMethodName, CodeBlock args) { - if (!dependsOnBean) { + 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, targetClass, 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) { @@ -390,16 +378,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(); @@ -411,59 +401,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; - - public ProxyRuntimeHintsRegistrar(AutowireCandidateResolver candidateResolver) { - this.candidateResolver = candidateResolver; - } + private record ProxyRuntimeHintsRegistrar(AutowireCandidateResolver 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)) { @@ -472,4 +445,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/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java index bf7c97a915d0..41631ad6a3b6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java @@ -1,9 +1,7 @@ /** * AOT support for bean factories. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.aot; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/AbstractFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/AbstractFactoryBean.java index ea7958d4d53f..0fb5d4371208 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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,13 @@ 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; + private @Nullable T singletonInstance; - @Nullable - private T singletonInstance; - - @Nullable - private T earlySingletonInstance; + private @Nullable T earlySingletonInstance; /** @@ -109,8 +103,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; } @@ -138,7 +131,6 @@ protected TypeConverter getBeanTypeConverter() { @Override public void afterPropertiesSet() throws Exception { if (isSingleton()) { - this.initialized = true; this.singletonInstance = createInstance(); this.earlySingletonInstance = null; } @@ -151,10 +143,10 @@ public void afterPropertiesSet() throws Exception { * @see #getEarlySingletonInterfaces() */ @Override - @SuppressWarnings("NullAway") public final T getObject() throws Exception { if (isSingleton()) { - return (this.initialized ? this.singletonInstance : getEarlySingletonInstance()); + T instance = this.singletonInstance; + return (instance != null ? instance : getEarlySingletonInstance()); } else { return createInstance(); @@ -166,7 +158,7 @@ public final T getObject() throws Exception { * circular reference. Not called in a non-circular scenario. */ @SuppressWarnings("unchecked") - private T getEarlySingletonInstance() throws Exception { + private T getEarlySingletonInstance() { Class[] ifcs = getEarlySingletonInterfaces(); if (ifcs == null) { throw new FactoryBeanNotInitializedException( @@ -184,10 +176,10 @@ 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 { - Assert.state(this.initialized, "Singleton instance not initialized yet"); - return this.singletonInstance; + T instance = this.singletonInstance; + Assert.state(instance != null, "Singleton instance not initialized yet"); + return instance; } /** @@ -197,7 +189,10 @@ private T getSingletonInstance() throws IllegalStateException { @Override public void destroy() throws Exception { if (isSingleton()) { - destroyInstance(this.singletonInstance); + T instance = this.singletonInstance; + if (instance != null) { + destroyInstance(instance); + } } } @@ -208,8 +203,7 @@ public void destroy() throws Exception { * @see org.springframework.beans.factory.FactoryBean#getObjectType() */ @Override - @Nullable - public abstract Class getObjectType(); + public abstract @Nullable Class getObjectType(); /** * Template method that subclasses must override to construct @@ -234,8 +228,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); } @@ -249,7 +242,7 @@ protected Class[] getEarlySingletonInterfaces() { * @throws Exception in case of shutdown errors * @see #createInstance() */ - protected void destroyInstance(@Nullable T instance) throws Exception { + protected void destroyInstance(T instance) throws Exception { } @@ -268,7 +261,7 @@ else if (ReflectionUtils.isHashCodeMethod(method)) { // Use hashCode of reference proxy. return System.identityHashCode(proxy); } - else if (!initialized && ReflectionUtils.isToStringMethod(method)) { + else if (ReflectionUtils.isToStringMethod(method) && singletonInstance == null) { return "Early singleton proxy for interfaces " + ObjectUtils.nullSafeToString(getEarlySingletonInterfaces()); } 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..161814b8733d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,13 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.lang.Nullable; /** * Extension of the {@link org.springframework.beans.factory.BeanFactory} @@ -96,16 +97,16 @@ public interface AutowireCapableBeanFactory extends BeanFactory { * Constant that indicates determining an appropriate autowire strategy * through introspection of the bean class. * @see #autowire - * @deprecated as of Spring 3.0: If you are using mixed autowiring strategies, - * prefer annotation-based autowiring for clearer demarcation of autowiring needs. + * @deprecated If you are using mixed autowiring strategies, prefer + * annotation-based autowiring for clearer demarcation of autowiring needs. */ - @Deprecated + @Deprecated(since = "3.0") int AUTOWIRE_AUTODETECT = 4; /** * 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; @@ -187,7 +188,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory { * @see #AUTOWIRE_BY_NAME * @see #AUTOWIRE_BY_TYPE * @see #AUTOWIRE_CONSTRUCTOR - * @deprecated as of 6.1, in favor of {@link #createBean(Class)} + * @deprecated in favor of {@link #createBean(Class)} */ @Deprecated(since = "6.1") Object createBean(Class beanClass, int autowireMode, boolean dependencyCheck) throws BeansException; @@ -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..a4257c06c7a2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple marker class for an individually autowired property value, to be added diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java index 0f6f6ab5cb66..9c4ed3b9f8e8 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-2024 the original author or authors. + * Copyright 2002-present the original author 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. @@ -155,13 +153,12 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * constructor arguments. This property should just be necessary for other kinds * of dependencies like statics (*ugh*) or database preparation on startup. */ - void setDependsOn(@Nullable String... dependsOn); + void setDependsOn(String @Nullable ... dependsOn); /** * Return the bean names that this bean depends on. */ - @Nullable - String[] getDependsOn(); + String @Nullable [] getDependsOn(); /** * Set whether this bean is a candidate for getting autowired into some other bean. @@ -222,8 +219,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * @see #getFactoryMethodName() * @see #getBeanClassName() */ - @Nullable - String getFactoryBeanName(); + @Nullable String getFactoryBeanName(); /** * Specify a factory method, if any. This method will be invoked with @@ -240,8 +236,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * @see #getFactoryBeanName() * @see #getBeanClassName() */ - @Nullable - String getFactoryMethodName(); + @Nullable String getFactoryMethodName(); /** * Return the constructor argument values for this bean. @@ -285,8 +280,7 @@ default boolean hasPropertyValues() { * Return the name of the initializer method. * @since 5.1 */ - @Nullable - String getInitMethodName(); + @Nullable String getInitMethodName(); /** * Set the name of the destroy method. @@ -298,8 +292,7 @@ default boolean hasPropertyValues() { * Return the name of the destroy method. * @since 5.1 */ - @Nullable - String getDestroyMethodName(); + @Nullable String getDestroyMethodName(); /** * Set the role hint for this {@code BeanDefinition}. The role hint @@ -331,8 +324,7 @@ default boolean hasPropertyValues() { /** * Return a human-readable description of this bean definition. */ - @Nullable - String getDescription(); + @Nullable String getDescription(); // Read-only attributes @@ -373,8 +365,7 @@ default boolean hasPropertyValues() { * Return a description of the resource that this bean definition * came from (for the purpose of showing context in case of errors). */ - @Nullable - String getResourceDescription(); + @Nullable String getResourceDescription(); /** * Return the originating BeanDefinition, or {@code null} if none. @@ -382,7 +373,6 @@ default boolean hasPropertyValues() { *

    Note that this method returns the immediate originator. Iterate through the * originator chain to find the original BeanDefinition as defined by the user. */ - @Nullable - BeanDefinition getOriginatingBeanDefinition(); + @Nullable BeanDefinition getOriginatingBeanDefinition(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionCustomizer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionCustomizer.java index 88d22c7af219..d9e86d68b9d5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionCustomizer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..b3e0d018aab0 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..4b47bb37573b 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-present the original author 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..83ae45de35ff 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..2fe8779b5086 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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/BeanFactoryPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java index 68c286cffb09..aba123e59d8a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,13 @@ * A {@code BeanFactoryPostProcessor} may also be registered programmatically * with a {@code ConfigurableApplicationContext}. * + *

    When registering a {@code BeanFactoryPostProcessor} via an {@code @Bean} method + * in a {@code @Configuration} class, use a {@code static} method to avoid eager + * initialization of other beans in the configuration class. See the + * "BeanFactoryPostProcessor-returning {@code @Bean} methods" section in + * {@link org.springframework.context.annotation.Bean @Bean}'s javadoc for details + * and an example. + * *

    Ordering

    *

    {@code BeanFactoryPostProcessor} beans that are autodetected in an * {@code ApplicationContext} will be ordered according to 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..ee05336a816d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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 — @@ -34,6 +35,14 @@ * created. A plain {@code BeanFactory} allows for programmatic registration of * post-processors, applying them to all beans created through the bean factory. * + *

    When registering a {@code BeanPostProcessor} via an {@code @Bean} method in + * a {@code @Configuration} class, use a {@code static} method with ideally no + * dependencies in order to avoid eager initialization that can make other beans + * ineligible for full post-processing. See the "BeanPostProcessor-returning + * {@code @Bean} methods" section in + * {@link org.springframework.context.annotation.Bean @Bean}'s javadoc for details + * and an example. + * *

    Ordering

    *

    {@code BeanPostProcessor} beans that are autodetected in an * {@code ApplicationContext} will be ordered according to @@ -70,8 +79,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; } @@ -81,7 +89,7 @@ default Object postProcessBeforeInitialization(Object bean, String beanName) thr * or a custom init-method). The bean will already be populated with property values. * The returned bean instance may be a wrapper around the original. *

    In case of a FactoryBean, this callback will be invoked for both the FactoryBean - * instance and the objects created by the FactoryBean (as of Spring 2.0). The + * instance and the objects created by the FactoryBean. The * post-processor can decide whether to apply to either the FactoryBean or created * objects or both through corresponding {@code bean instanceof FactoryBean} checks. *

    This callback will also be invoked after a short-circuiting triggered by a @@ -96,8 +104,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/BeanReference.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanReference.java index 69f490977a88..40cc02d893c3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanReference.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 8b46ee31b230..842738d18739 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.beans.PropertyEditor; import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.TypeConverter; @@ -28,7 +30,6 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.core.convert.ConversionService; import org.springframework.core.metrics.ApplicationStartup; -import org.springframework.lang.Nullable; import org.springframework.util.StringValueResolver; /** @@ -94,8 +95,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * (only {@code null} if even the system ClassLoader isn't accessible). * @see org.springframework.util.ClassUtils#forName(String, ClassLoader) */ - @Nullable - ClassLoader getBeanClassLoader(); + @Nullable ClassLoader getBeanClassLoader(); /** * Specify a temporary ClassLoader to use for type matching purposes. @@ -113,8 +113,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * if any. * @since 2.5 */ - @Nullable - ClassLoader getTempClassLoader(); + @Nullable ClassLoader getTempClassLoader(); /** * Set whether to cache bean metadata such as given bean definitions @@ -144,8 +143,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * Return the resolution strategy for expressions in bean definition values. * @since 3.0 */ - @Nullable - BeanExpressionResolver getBeanExpressionResolver(); + @Nullable BeanExpressionResolver getBeanExpressionResolver(); /** * Set the {@link Executor} (possibly a {@link org.springframework.core.task.TaskExecutor}) @@ -160,8 +158,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * for background bootstrapping, if any. * @since 6.2 */ - @Nullable - Executor getBootstrapExecutor(); + @Nullable Executor getBootstrapExecutor(); /** * Specify a {@link ConversionService} to use for converting @@ -174,8 +171,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * Return the associated ConversionService, if any. * @since 3.0 */ - @Nullable - ConversionService getConversionService(); + @Nullable ConversionService getConversionService(); /** * Add a PropertyEditorRegistrar to be applied to all bean creation processes. @@ -183,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); @@ -241,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 @@ -255,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 */ @@ -290,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..9df8898f156d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,11 @@ import java.util.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 @@ -152,6 +153,18 @@ boolean isAutowireCandidate(String beanName, DependencyDescriptor descriptor) */ boolean isConfigurationFrozen(); + /** + * Mark current thread as main bootstrap thread for singleton instantiation, + * with lenient bootstrap locking applying for background threads. + *

    Any such marker is to be removed at the end of the managed bootstrap in + * {@link #preInstantiateSingletons()}. + * @since 6.2.12 + * @see #setBootstrapExecutor + * @see #preInstantiateSingletons() + */ + default void prepareSingletonBootstrap() { + } + /** * Ensure that all non-lazy-init singletons are instantiated, also considering * {@link org.springframework.beans.factory.FactoryBean FactoryBeans}. @@ -159,6 +172,7 @@ boolean isAutowireCandidate(String beanName, DependencyDescriptor descriptor) * @throws BeansException if one of the singleton beans could not be created. * Note: This may have left the factory with some beans already initialized! * Call {@link #destroySingletons()} for full cleanup in this case. + * @see #prepareSingletonBootstrap() * @see #destroySingletons() */ void preInstantiateSingletons() throws BeansException; 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..89a943b62d47 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..f53577834ac9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.beans.BeansException; import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -33,7 +33,7 @@ * registration of custom {@link PropertyEditor property editors}. * *

    In case you want to register {@link PropertyEditor} instances, - * the recommended usage as of Spring 2.0 is to use custom + * the recommended usage is to use custom * {@link PropertyEditorRegistrar} implementations that in turn register any * desired editor instances on a given * {@link org.springframework.beans.PropertyEditorRegistry registry}. Each @@ -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..7d2125f616aa 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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 260f7807ea51..50856d62847f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,24 +19,22 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; -import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.Map; import java.util.Optional; -import kotlin.reflect.KProperty; -import kotlin.reflect.jvm.ReflectJvmMapping; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; +import org.springframework.core.Nullness; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -52,16 +50,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 +64,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,19 +140,19 @@ 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; } /** * Return whether this dependency is required. - *

    Optional semantics are derived from Java 8's {@link java.util.Optional}, - * any variant of a parameter-level {@code Nullable} annotation (such as from - * JSR-305 or the FindBugs set of annotations), or a language-level nullable - * type declaration in Kotlin. + *

    Optional semantics are derived from Java's {@link java.util.Optional}, + * any variant of a parameter-level {@code @Nullable} annotation (such as from + * JSpecify, JSR-305, or the FindBugs set of annotations), or a language-level + * nullable type declaration in Kotlin. */ public boolean isRequired() { if (!this.required) { @@ -168,30 +160,13 @@ public boolean isRequired() { } if (this.field != null) { - return !(this.field.getType() == Optional.class || hasNullableAnnotation() || - (KotlinDetector.isKotlinReflectPresent() && - KotlinDetector.isKotlinType(this.field.getDeclaringClass()) && - KotlinDelegate.isNullable(this.field))); + return !(this.field.getType() == Optional.class || Nullness.forField(this.field) == Nullness.NULLABLE); } else { return !obtainMethodParameter().isOptional(); } } - /** - * Check whether the underlying field is annotated with any variant of a - * {@code Nullable} annotation, e.g. {@code jakarta.annotation.Nullable} or - * {@code edu.umd.cs.findbugs.annotations.Nullable}. - */ - private boolean hasNullableAnnotation() { - for (Annotation ann : getAnnotations()) { - if ("Nullable".equals(ann.annotationType().getSimpleName())) { - return true; - } - } - return false; - } - /** * Return whether this dependency is 'eager' in the sense of * eagerly resolving potential target beans for type matching. @@ -213,8 +188,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,15 +204,14 @@ 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; } /** * Resolve the specified bean name, as a candidate result of the matching * algorithm for this dependency, to a bean instance from the given factory. - *

    The default implementation calls {@link BeanFactory#getBean(String)}. + *

    The default implementation calls {@link BeanFactory#getBean(String, Class)}. * Subclasses may provide additional arguments or other customizations. * @param beanName the bean name, as a candidate result for this dependency * @param requiredType the expected type of the bean (as an assertion) @@ -251,7 +224,14 @@ public Object resolveShortcut(BeanFactory beanFactory) throws BeansException { public Object resolveCandidate(String beanName, Class requiredType, BeanFactory beanFactory) throws BeansException { - return beanFactory.getBean(beanName); + try { + // Need to provide required type for SmartFactoryBean + return beanFactory.getBean(beanName, requiredType); + } + catch (BeanNotOfRequiredTypeException ex) { + // Probably a null bean... + return beanFactory.getBean(beanName); + } } @@ -355,8 +335,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()); } @@ -381,7 +360,7 @@ public Class getDependencyType() { /** * Determine whether this dependency supports lazy resolution, - * e.g. through extra proxying. The default is {@code true}. + * for example, through extra proxying. The default is {@code true}. * @since 6.1.2 * @see org.springframework.beans.factory.support.AutowireCandidateResolver#getLazyResolutionProxyIfNecessary */ @@ -456,19 +435,4 @@ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFound } } - - /** - * Inner class to avoid a hard dependency on Kotlin at runtime. - */ - private static class KotlinDelegate { - - /** - * Check whether the specified {@link Field} represents a nullable Kotlin type or not. - */ - public static boolean isNullable(Field field) { - KProperty property = ReflectJvmMapping.getKotlinProperty(field); - return (property != null && property.getReturnType().isMarkedNullable()); - } - } - } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DeprecatedBeanWarner.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DeprecatedBeanWarner.java index ee05ce45d52a..4c044b01624e 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..24ab3fab0605 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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 @@ 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..464601da9b89 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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 75cc184f3c04..0ccec8cfd629 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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,7 +158,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void afterPropertiesSet() throws ClassNotFoundException, NoSuchFieldException { if (this.targetClass != null && this.targetObject != null) { throw new IllegalArgumentException("Specify either targetClass or targetObject, not both"); @@ -190,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); @@ -210,8 +201,7 @@ else if (this.targetField == null) { @Override - @Nullable - public Object getObject() throws IllegalAccessException { + public @Nullable Object getObject() throws IllegalAccessException { if (this.fieldObject == null) { throw new FactoryBeanNotInitializedException(); } @@ -227,8 +217,7 @@ public Object getObject() throws IllegalAccessException { } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { return (this.fieldObject != null ? this.fieldObject.getType() : null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java index e842353cb55d..a1143b41ce45 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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.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..849b8aaf88dd 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..093a44b0d6f7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,11 @@ import java.util.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..77bfac8f084f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.InvocationTargetException; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.support.ArgumentConvertingMethodInvoker; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -67,11 +68,9 @@ public class MethodInvokingBean extends ArgumentConvertingMethodInvoker implements BeanClassLoaderAware, BeanFactoryAware, InitializingBean { - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private ConfigurableBeanFactory beanFactory; + private @Nullable ConfigurableBeanFactory beanFactory; @Override @@ -117,8 +116,7 @@ public void afterPropertiesSet() throws Exception { * Perform the invocation and convert InvocationTargetException * into the underlying target exception. */ - @Nullable - protected Object invokeWithTargetException() throws Exception { + protected @Nullable Object invokeWithTargetException() throws Exception { try { return invoke(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java index ec89f904e4f4..5083a28ae590 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBeanNotInitializedException; -import org.springframework.lang.Nullable; /** * {@link FactoryBean} which returns a value which is the result of a static or instance @@ -88,8 +89,7 @@ public class MethodInvokingFactoryBean extends MethodInvokingBean implements Fac private boolean initialized = false; /** Method call result in the singleton case. */ - @Nullable - private Object singletonObject; + private @Nullable Object singletonObject; /** @@ -116,8 +116,7 @@ public void afterPropertiesSet() throws Exception { * specified method on the fly. */ @Override - @Nullable - public Object getObject() throws Exception { + public @Nullable Object getObject() throws Exception { if (this.singleton) { if (!this.initialized) { throw new FactoryBeanNotInitializedException(); @@ -136,8 +135,7 @@ public Object getObject() throws Exception { * or {@code null} if not known in advance. */ @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { if (!isPrepared()) { // Not fully initialized yet -> return null to indicate "not known yet". return null; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/NamedBeanHolder.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/NamedBeanHolder.java index ca73a408fbed..85f3c55d6b9b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/NamedBeanHolder.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/NamedBeanHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..a3ff2ac09d43 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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 fe6ac67ef80e..6d610b91b863 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.beans.factory.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; -import org.springframework.lang.Nullable; +import org.springframework.core.env.AbstractPropertyResolver; import org.springframework.util.StringValueResolver; +import org.springframework.util.SystemPropertyUtils; /** * Abstract base class for property resource configurers that resolve placeholders @@ -37,16 +40,16 @@ * *
      * <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    - *   <property name="driverClassName" value="${driver}" />
    - *   <property name="url" value="jdbc:${dbname}" />
    + *   <property name="driverClassName" value="${jdbc.driver}" />
    + *   <property name="url" value="jdbc:${jdbc.dbname}" />
      * </bean>
      * 
    * * Example properties file: * *
    - * driver=com.mysql.jdbc.Driver
    - * dbname=mysql:mydb
    + * jdbc.driver=com.mysql.jdbc.Driver + * jdbc.dbname=mysql:mydb * * Annotated bean definitions may take advantage of property replacement using * the {@link org.springframework.beans.factory.annotation.Value @Value} annotation: @@ -79,11 +82,12 @@ *

    Example XML property with default value: * *

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

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

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

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

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

    The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}. * @since 6.2 */ - public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) { - this.escapeCharacter = escsEscapeCharacter; + public void setEscapeCharacter(@Nullable Character escapeCharacter) { + this.escapeCharacter = escapeCharacter; } /** @@ -180,7 +187,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 @@ -228,7 +235,6 @@ public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } - @SuppressWarnings("NullAway") protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PreferencesPlaceholderConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PreferencesPlaceholderConfigurer.java index fc616b895ef9..81abead0275e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PreferencesPlaceholderConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PreferencesPlaceholderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,14 @@ import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** - * Subclass of PropertyPlaceholderConfigurer that supports JDK 1.4's - * Preferences API ({@code java.util.prefs}). + * Subclass of {@link PropertyPlaceholderConfigurer} that supports JDK 1.4's + * {@link Preferences} API. * *

    Tries to resolve placeholders as keys first in the user preferences, * then in the system preferences, then in this configurer's properties. @@ -42,16 +43,15 @@ * @see #setSystemTreePath * @see #setUserTreePath * @see java.util.prefs.Preferences - * @deprecated as of 5.2, along with {@link PropertyPlaceholderConfigurer} + * @deprecated as of 5.2, along with {@link PropertyPlaceholderConfigurer}; to be removed in 8.0 */ -@Deprecated +@Deprecated(since = "5.2", forRemoval = true) +@SuppressWarnings({"deprecation", "removal"}) public class PreferencesPlaceholderConfigurer extends PropertyPlaceholderConfigurer implements InitializingBean { - @Nullable - private String systemTreePath; + private @Nullable String systemTreePath; - @Nullable - private String userTreePath; + private @Nullable String userTreePath; private Preferences systemPrefs = Preferences.systemRoot(); @@ -120,8 +120,7 @@ protected String resolvePlaceholder(String placeholder, Properties props) { * @param preferences the Preferences to resolve against * @return the value for the placeholder, or {@code null} if none found */ - @Nullable - protected String resolvePlaceholder(@Nullable String path, String key, Preferences preferences) { + protected @Nullable String resolvePlaceholder(@Nullable String path, String key, Preferences preferences) { if (path != null) { // Do not create the node if it does not exist... try { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertiesFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertiesFactoryBean.java index 47a857eb1f24..f122ea3d806f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.io.IOException; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.support.PropertiesLoaderSupport; -import org.springframework.lang.Nullable; /** * Allows for making a properties file from a classpath location available @@ -48,8 +49,7 @@ public class PropertiesFactoryBean extends PropertiesLoaderSupport private boolean singleton = true; - @Nullable - private Properties singletonInstance; + private @Nullable Properties singletonInstance; /** @@ -75,8 +75,7 @@ public final void afterPropertiesSet() throws IOException { } @Override - @Nullable - public final Properties getObject() throws IOException { + public final @Nullable Properties getObject() throws IOException { if (this.singleton) { return this.singletonInstance; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java index 840a34e76234..edc89eaf32ad 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,12 +35,14 @@ * * Example properties file: * - *

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

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

    Note that the context definition is not aware of being overridden; * so this is not immediately obvious when looking at the XML definition file. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java index 4af0a37f886a..4ee7cc9e70b6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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,17 @@ public class PropertyPathFactoryBean implements FactoryBean, BeanNameAwa private static final Log logger = LogFactory.getLog(PropertyPathFactoryBean.class); - @Nullable - private BeanWrapper targetBeanWrapper; + private @Nullable BeanWrapper targetBeanWrapper; - @Nullable - private String targetBeanName; + private @Nullable String targetBeanName; - @Nullable - private String propertyPath; + private @Nullable String propertyPath; - @Nullable - private Class resultType; + private @Nullable Class resultType; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** @@ -121,7 +115,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 +125,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); @@ -162,28 +156,29 @@ public void setBeanName(String beanName) { @Override - @SuppressWarnings("NullAway") public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; + String targetBeanName = this.targetBeanName; - if (this.targetBeanWrapper != null && this.targetBeanName != null) { + if (this.targetBeanWrapper != null && targetBeanName != null) { throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both"); } - if (this.targetBeanWrapper == null && this.targetBeanName == null) { + if (this.targetBeanWrapper == null && targetBeanName == null) { if (this.propertyPath != null) { throw new IllegalArgumentException( "Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'"); } // No other properties specified: check bean name. - int dotIndex = (this.beanName != null ? this.beanName.indexOf('.') : -1); - if (dotIndex == -1) { + int dotIndex; + if (this.beanName == null || (dotIndex = this.beanName.indexOf('.')) <= 0) { throw new IllegalArgumentException( "Neither 'targetObject' nor 'targetBeanName' specified, and PropertyPathFactoryBean " + "bean name '" + this.beanName + "' does not follow 'beanName.property' syntax"); } - this.targetBeanName = this.beanName.substring(0, dotIndex); + targetBeanName = this.beanName.substring(0, dotIndex); + this.targetBeanName = targetBeanName; this.propertyPath = this.beanName.substring(dotIndex + 1); } @@ -192,9 +187,10 @@ else if (this.propertyPath == null) { throw new IllegalArgumentException("'propertyPath' is required"); } - if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) { + if (this.targetBeanWrapper == null && StringUtils.hasLength(targetBeanName) && + this.beanFactory.isSingleton(targetBeanName)) { // Eagerly fetch singleton target bean, and determine result type. - Object bean = this.beanFactory.getBean(this.targetBeanName); + Object bean = this.beanFactory.getBean(targetBeanName); this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean); this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath); } @@ -202,8 +198,7 @@ else if (this.propertyPath == null) { @Override - @Nullable - public Object getObject() throws BeansException { + public @Nullable Object getObject() throws BeansException { BeanWrapper target = this.targetBeanWrapper; if (target != null) { if (logger.isWarnEnabled() && this.targetBeanName != null && @@ -225,8 +220,7 @@ public Object getObject() throws BeansException { } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { return this.resultType; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java index 6e23f7fdeca5..52eecbb2b7ec 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,11 @@ import java.util.Map; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.core.SpringProperties; import org.springframework.core.env.AbstractEnvironment; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -51,11 +52,13 @@ * @see #setSystemPropertiesModeName * @see PlaceholderConfigurerSupport * @see PropertyOverrideConfigurer - * @deprecated as of 5.2; use {@code org.springframework.context.support.PropertySourcesPlaceholderConfigurer} - * instead which is more flexible through taking advantage of the {@link org.springframework.core.env.Environment} - * and {@link org.springframework.core.env.PropertySource} mechanisms. + * @deprecated as of 5.2, to be removed in 8.0; + * use {@code org.springframework.context.support.PropertySourcesPlaceholderConfigurer} + * instead which is more flexible through taking advantage of the + * {@link org.springframework.core.env.Environment} and + * {@link org.springframework.core.env.PropertySource} mechanisms. */ -@Deprecated +@Deprecated(since = "5.2", forRemoval = true) public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport { /** Never check system properties. */ @@ -93,7 +96,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 +156,7 @@ public void setSearchSystemEnvironment(boolean searchSystemEnvironment) { * @see System#getProperty * @see #resolvePlaceholder(String, java.util.Properties) */ - @Nullable - protected String resolvePlaceholder(String placeholder, Properties props, int systemPropertiesMode) { + protected @Nullable String resolvePlaceholder(String placeholder, Properties props, int systemPropertiesMode) { String propVal = null; if (systemPropertiesMode == SYSTEM_PROPERTIES_MODE_OVERRIDE) { propVal = resolveSystemProperty(placeholder); @@ -181,8 +183,7 @@ protected String resolvePlaceholder(String placeholder, Properties props, int sy * @return the resolved value, of {@code null} if none * @see #setSystemPropertiesMode */ - @Nullable - protected String resolvePlaceholder(String placeholder, Properties props) { + protected @Nullable String resolvePlaceholder(String placeholder, Properties props) { return props.getProperty(placeholder); } @@ -195,8 +196,7 @@ protected String resolvePlaceholder(String placeholder, Properties props) { * @see System#getProperty(String) * @see System#getenv(String) */ - @Nullable - protected String resolveSystemProperty(String key) { + protected @Nullable String resolveSystemProperty(String key) { try { String value = System.getProperty(key); if (value == null && this.searchSystemEnvironment) { @@ -240,8 +240,7 @@ public PlaceholderResolvingStringValueResolver(Properties props) { } @Override - @Nullable - public String resolveStringValue(String strVal) throws BeansException { + public @Nullable String resolveStringValue(String strVal) throws BeansException { String resolved = this.helper.replacePlaceholders(strVal, this.resolver); if (trimValues) { resolved = resolved.trim(); @@ -260,8 +259,7 @@ private PropertyPlaceholderConfigurerResolver(Properties props) { } @Override - @Nullable - public String resolvePlaceholder(String placeholderName) { + public @Nullable String resolvePlaceholder(String placeholderName) { return PropertyPlaceholderConfigurer.this.resolvePlaceholder(placeholderName, this.props, systemPropertiesMode); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java index d6de515fa3f5..487433ac1b85 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..0119b89d464f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..a9c77f79af75 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..17cd3a9e014d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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; /** @@ -88,6 +87,33 @@ public RuntimeBeanReference(Class beanType, boolean toParent) { this.toParent = toParent; } + /** + * Create a new RuntimeBeanReference to a bean of the given type. + * @param beanName name of the target bean + * @param beanType type of the target bean + * @since 7.0 + */ + public RuntimeBeanReference(String beanName, Class beanType) { + this(beanName, beanType, false); + } + + /** + * Create a new RuntimeBeanReference to a bean of the given type, + * with the option to mark it as reference to a bean in the parent factory. + * @param beanName name of the target bean + * @param beanType type of the target bean + * @param toParent whether this is an explicit reference to a bean in the + * parent factory + * @since 7.0 + */ + public RuntimeBeanReference(String beanName, Class beanType, boolean toParent) { + Assert.hasText(beanName, "'beanName' must not be empty"); + Assert.notNull(beanType, "'beanType' must not be null"); + this.beanName = beanName; + this.beanType = beanType; + this.toParent = toParent; + } + /** * Return the requested bean name, or the fully-qualified type name @@ -103,8 +129,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 +149,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..6a0a4de51e21 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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,15 @@ 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". + *

    Since 7.0, this interface method returns {@code null} by default. * @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); + default @Nullable Object resolveContextualObject(String key) { + return null; + } /** * Return the conversation ID for the current underlying scope, if any. @@ -144,11 +146,13 @@ public interface Scope { *

    Note: This is an optional operation. It is perfectly valid to * return {@code null} in an implementation of this method if the * underlying storage mechanism has no obvious candidate for such an ID. + *

    Since 7.0, this interface method returns {@code null} by default. * @return the conversation ID, or {@code null} if there is no * conversation ID for the current scope * @throws IllegalStateException if the underlying scope is not currently active */ - @Nullable - String getConversationId(); + default @Nullable String getConversationId() { + return null; + } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java index 590280998d59..fd7b879583d4 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-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.lang.reflect.Proxy; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.FatalBeanException; @@ -30,7 +32,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -190,20 +191,15 @@ */ public class ServiceLocatorFactoryBean implements FactoryBean, BeanFactoryAware, InitializingBean { - @Nullable - private Class serviceLocatorInterface; + private @Nullable Class serviceLocatorInterface; - @Nullable - private Constructor serviceLocatorExceptionConstructor; + private @Nullable Constructor serviceLocatorExceptionConstructor; - @Nullable - private Properties serviceMappings; + private @Nullable Properties serviceMappings; - @Nullable - private ListableBeanFactory beanFactory; + private @Nullable ListableBeanFactory beanFactory; - @Nullable - private Object proxy; + private @Nullable Object proxy; /** @@ -315,7 +311,7 @@ protected Constructor determineServiceLocatorExceptionConstructor(Cla */ protected Exception createServiceLocatorException(Constructor exceptionConstructor, BeansException cause) { Class[] paramTypes = exceptionConstructor.getParameterTypes(); - Object[] args = new Object[paramTypes.length]; + @Nullable Object[] args = new Object[paramTypes.length]; for (int i = 0; i < paramTypes.length; i++) { if (String.class == paramTypes[i]) { args[i] = cause.getMessage(); @@ -329,14 +325,12 @@ else if (paramTypes[i].isInstance(cause)) { @Override - @Nullable - public Object getObject() { + public @Nullable Object getObject() { return this.proxy; } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { return this.serviceLocatorInterface; } @@ -394,7 +388,7 @@ private Object invokeServiceLocatorMethod(Method method, Object[] args) throws E /** * Check whether a service id was passed in. */ - private String tryGetBeanName(@Nullable Object[] args) { + private String tryGetBeanName(Object @Nullable [] args) { String beanName = ""; if (args != null && args.length == 1 && args[0] != null) { beanName = args[0].toString(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java index 8b30f8eb8f48..9351275da55a 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,11 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -35,12 +36,10 @@ */ public class SetFactoryBean extends AbstractFactoryBean> { - @Nullable - private Set sourceSet; + private @Nullable Set sourceSet; @SuppressWarnings("rawtypes") - @Nullable - private Class targetSetClass; + private @Nullable Class targetSetClass; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java index b1f9f876b425..3bac88aa954a 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.function.Consumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface that defines a registry for shared bean instances. @@ -83,8 +83,7 @@ public interface SingletonBeanRegistry { * @return the registered singleton object, or {@code null} if none found * @see ConfigurableListableBeanFactory#getBeanDefinition */ - @Nullable - Object getSingleton(String beanName); + @Nullable Object getSingleton(String beanName); /** * Check if this registry contains a singleton instance with the given name. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java index 86455b173c11..494ce6ba76e3 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; 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..07734f0dc8dd 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,9 @@ import java.util.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..f25ce93707e7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,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 @@ -64,7 +65,7 @@ * Note that the value of "foo" in the first document is not simply replaced * with the value in the second, but its nested values are merged. * - *

    Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1. + *

    Requires SnakeYAML 2.0 or higher. * * @author Dave Syer * @author Juergen Hoeller @@ -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..96ce40db01a1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.Map; import java.util.Properties; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; 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 +44,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; @@ -50,7 +51,7 @@ /** * Base class for YAML factories. * - *

    Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1. + *

    Requires SnakeYAML 2.0 or higher. * * @author Dave Syer * @author Juergen Hoeller @@ -77,7 +78,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
    @@ -194,30 +195,31 @@ protected Yaml createYaml() {
     	}
     
     	private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
    -		int count = 0;
    +		AtomicInteger count = new AtomicInteger();
     		try {
     			if (logger.isDebugEnabled()) {
     				logger.debug("Loading from YAML: " + resource);
     			}
    -			try (Reader reader = new UnicodeReader(resource.getInputStream())) {
    +			resource.consumeContent(inputStream -> {
    +				Reader reader = new UnicodeReader(inputStream);
     				for (Object object : yaml.loadAll(reader)) {
     					if (object != null && process(asMap(object), callback)) {
    -						count++;
    +						count.incrementAndGet();
     						if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
     							break;
     						}
     					}
     				}
     				if (logger.isDebugEnabled()) {
    -					logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
    +					logger.debug("Loaded " + count + " document" + (count.get() > 1 ? "s" : "") +
     							" from YAML resource: " + resource);
     				}
    -			}
    +			});
     		}
     		catch (IOException ex) {
     			handleProcessError(resource, ex);
     		}
    -		return (count > 0);
    +		return (count.get() > 0);
     	}
     
     	private void handleProcessError(Resource resource, IOException ex) {
    @@ -249,7 +251,7 @@ private Map asMap(Object object) {
     			}
     			else {
     				// It has to be a map key in this case
    -				result.put("[" + key.toString() + "]", value);
    +				result.put("[" + key + "]", value);
     			}
     		});
     		return result;
    @@ -304,13 +306,37 @@ private boolean process(Map map, MatchCallback callback) {
     	 * @since 4.1.3
     	 */
     	protected final Map getFlattenedMap(Map source) {
    +		return getFlattenedMap(source, false, null);
    +	}
    +
    +	/**
    +	 * Return a flattened version of the given map, recursively following any nested Map
    +	 * or Collection values. Entries from the resulting map retain the same order as the
    +	 * source. When called with the Map from a {@link MatchCallback} the result will
    +	 * contain the same values as the {@link MatchCallback} Properties.
    +	 * @param source the source map
    +	 * @param includeEmpty whether empty entries should be included in the result
    +	 * @param emptyValue the value used to represent an empty entry — for
    +	 * example, {@code null} or an empty {@code String}
    +	 * @return a flattened map
    +	 * @since 7.0.4
    +	 */
    +	protected final Map getFlattenedMap(Map source, boolean includeEmpty,
    +			@Nullable Object emptyValue) {
    +
     		Map result = new LinkedHashMap<>();
    -		buildFlattenedMap(result, source, null);
    +		buildFlattenedMap(result, source, null, includeEmpty, emptyValue);
     		return result;
     	}
     
     	@SuppressWarnings({"rawtypes", "unchecked"})
    -	private void buildFlattenedMap(Map result, Map source, @Nullable String path) {
    +	private void buildFlattenedMap(Map result, Map source, @Nullable String path,
    +			boolean includeEmpty, @Nullable Object emptyValue) {
    +
    +		if (includeEmpty && source.isEmpty()) {
    +			result.put(path, emptyValue);
    +			return;
    +		}
     		source.forEach((key, value) -> {
     			if (StringUtils.hasText(path)) {
     				if (key.startsWith("[")) {
    @@ -325,7 +351,7 @@ private void buildFlattenedMap(Map result, Map s
     			}
     			else if (value instanceof Map map) {
     				// Need a compound key
    -				buildFlattenedMap(result, map, key);
    +				buildFlattenedMap(result, map, key, includeEmpty, emptyValue);
     			}
     			else if (value instanceof Collection collection) {
     				// Need a compound key
    @@ -336,12 +362,12 @@ else if (value instanceof Collection collection) {
     					int count = 0;
     					for (Object object : collection) {
     						buildFlattenedMap(result, Collections.singletonMap(
    -								"[" + (count++) + "]", object), key);
    +								"[" + (count++) + "]", object), key, includeEmpty, emptyValue);
     					}
     				}
     			}
     			else {
    -				result.put(key, (value != null ? value : ""));
    +				result.put(key, (value != null ? value : (includeEmpty ? emptyValue : "")));
     			}
     		});
     	}
    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..8eb30cce9da0 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
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2023 the original author or authors.
    + * Copyright 2002-present the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -18,10 +18,11 @@
     
     import java.util.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. * @@ -74,7 +75,7 @@ * servers[1]=foo.bar.com *

    * - *

    Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1. + *

    Requires SnakeYAML 2.0 or higher. * * @author Dave Syer * @author Stephane Nicoll @@ -85,8 +86,7 @@ public class YamlPropertiesFactoryBean extends YamlProcessor implements FactoryB private boolean singleton = true; - @Nullable - private Properties properties; + private @Nullable Properties properties; /** @@ -110,8 +110,7 @@ public void afterPropertiesSet() { } @Override - @Nullable - public Properties getObject() { + public @Nullable Properties getObject() { return (this.properties != null ? this.properties : createProperties()); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java index 280e916ab186..5ba69e387644 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java @@ -1,9 +1,7 @@ /** * SPI interfaces and configuration-related convenience classes for bean factories. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java index 0d9a67cd04b6..7fed5e830000 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,6 @@ import org.springframework.core.io.DescriptiveResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -151,11 +151,9 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp private MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(getClass()); - @Nullable - private Binding binding; + private @Nullable Binding binding; - @Nullable - private GroovyBeanDefinitionWrapper currentBeanDefinition; + private @Nullable GroovyBeanDefinitionWrapper currentBeanDefinition; /** @@ -207,8 +205,7 @@ public void setBinding(Binding binding) { /** * Return a specified binding for Groovy variables, if any. */ - @Nullable - public Binding getBinding() { + public @Nullable Binding getBinding() { return this.binding; } @@ -251,8 +248,7 @@ public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefin @SuppressWarnings("serial") Closure beans = new Closure<>(this) { @Override - @Nullable - public Object call(Object... args) { + public @Nullable Object call(Object... args) { invokeBeanDefiningClosure((Closure) args[0]); return null; } @@ -658,8 +654,7 @@ else if (value instanceof Closure callable) { * */ @Override - @Nullable - public Object getProperty(String name) { + public @Nullable Object getProperty(String name) { Binding binding = getBinding(); if (binding != null && binding.hasVariable(name)) { return binding.getVariable(name); @@ -701,7 +696,7 @@ else if (this.currentBeanDefinition != null) { } } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation private GroovyDynamicElementReader createDynamicElementReader(String namespace) { XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext( new DescriptiveResource("Groovy")); @@ -733,8 +728,7 @@ private static class DeferredProperty { private final String name; - @Nullable - public Object value; + public @Nullable Object value; public DeferredProperty(GroovyBeanDefinitionWrapper beanDefinition, String name, @Nullable Object value) { this.beanDefinition = beanDefinition; @@ -769,8 +763,7 @@ public MetaClass getMetaClass() { } @Override - @Nullable - public Object getProperty(String property) { + public @Nullable Object getProperty(String property) { if (property.equals("beanName")) { return getBeanName(); } @@ -809,8 +802,7 @@ private class GroovyPropertyValue extends GroovyObjectSupport { private final String propertyName; - @Nullable - private final Object propertyValue; + private final @Nullable Object propertyValue; public GroovyPropertyValue(String propertyName, @Nullable Object propertyValue) { this.propertyName = propertyName; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java index 895646d9da0b..90e3cb9b49e3 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-2024 the original author or authors. + * Copyright 2002-present the original author 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) { @@ -91,8 +85,7 @@ class GroovyBeanDefinitionWrapper extends GroovyObjectSupport { } - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } @@ -159,8 +152,7 @@ GroovyBeanDefinitionWrapper addProperty(String propertyName, @Nullable Object pr @Override - @Nullable - public Object getProperty(String property) { + public @Nullable Object getProperty(String property) { Assert.state(this.definitionWrapper != null, "BeanDefinition wrapper not initialized"); if (this.definitionWrapper.isReadableProperty(property)) { return this.definitionWrapper.getPropertyValue(property); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.java index b8b9efd52031..92414c555b77 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,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/AbstractComponentDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/AbstractComponentDefinition.java index d4fdc29706db..c8fd416f59d7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/AbstractComponentDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/AbstractComponentDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..c6701f30daca 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..6deb0da0efe4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,13 @@ import java.util.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 @@ -36,6 +37,11 @@ */ public class BeanComponentDefinition extends BeanDefinitionHolder implements ComponentDefinition { + private static final BeanDefinition[] EMPTY_BEAN_DEFINITION_ARRAY = new BeanDefinition[0]; + + private static final BeanReference[] EMPTY_BEAN_REFERENCE_ARRAY = new BeanReference[0]; + + private final BeanDefinition[] innerBeanDefinitions; private final BeanReference[] beanReferences; @@ -56,7 +62,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)); } @@ -83,8 +89,8 @@ else if (value instanceof BeanReference beanRef) { references.add(beanRef); } } - this.innerBeanDefinitions = innerBeans.toArray(new BeanDefinition[0]); - this.beanReferences = references.toArray(new BeanReference[0]); + this.innerBeanDefinitions = innerBeans.toArray(EMPTY_BEAN_DEFINITION_ARRAY); + this.beanReferences = references.toArray(EMPTY_BEAN_REFERENCE_ARRAY); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionParsingException.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionParsingException.java index 6cdf858b5009..11b03dd86aca 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionParsingException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionParsingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanEntry.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanEntry.java index ccba6d76e688..d7b92c44c1d1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanEntry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..88ad1bd473b3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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 @@ * 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..7b8cca7d7d80 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,8 @@ import java.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/ConstructorArgumentEntry.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntry.java index b6c5956d2c11..4a5dec2cdb6c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/DefaultsDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/DefaultsDefinition.java index 90b244249e88..16baa93a6f2a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/DefaultsDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/DefaultsDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/EmptyReaderEventListener.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/EmptyReaderEventListener.java index 397db05eb5ba..f200fcc035f5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/EmptyReaderEventListener.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/EmptyReaderEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..39ad9557aa2f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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,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..eb84bd7ddd67 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..0883538e3103 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..e9adc01a7dde 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.beans.factory.parsing; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * Simple implementation of {@link SourceExtractor} that returns {@code null} @@ -35,8 +36,7 @@ public class NullSourceExtractor implements SourceExtractor { * This implementation simply returns {@code null} for any input. */ @Override - @Nullable - public Object extractSource(Object sourceCandidate, @Nullable Resource definitionResource) { + public @Nullable Object extractSource(Object sourceCandidate, @Nullable Resource definitionResource) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java index afbb13957125..44fb3a06bc66 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-2023 the original author or authors. + * Copyright 2002-present the original author 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(); } 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..5ab54f501918 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..57c2bab906bd 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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/ProblemReporter.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ProblemReporter.java index b9ded86588ef..d7b67cb3122b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ProblemReporter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ProblemReporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/PropertyEntry.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/PropertyEntry.java index c20235a09b78..2917f16ee09c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/PropertyEntry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/PropertyEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/QualifierEntry.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/QualifierEntry.java index 45283e5838ce..25bc312296fe 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/QualifierEntry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/QualifierEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..50657c30a4ea 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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/ReaderEventListener.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ReaderEventListener.java index 24a4050034e2..040005ee942f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ReaderEventListener.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ReaderEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..0ece76cd49e3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..29cc7b19c845 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,10 @@ import java.util.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..65a5131c5261 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,9 @@ import java.util.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/ServiceListFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceListFactoryBean.java index 6e97d2f29c82..86d55b85575e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceListFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceListFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceLoaderFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceLoaderFactoryBean.java index 53c40efc24f6..9d1ed8711ccf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceLoaderFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceLoaderFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 c04d2289ab85..95bd2e43fa8c 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import java.util.function.Supplier; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; @@ -73,7 +74,6 @@ import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.PriorityOrdered; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -125,8 +125,7 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac private InstantiationStrategy instantiationStrategy; /** Resolver strategy for method parameter names. */ - @Nullable - private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + private @Nullable ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); /** Whether to automatically try to resolve circular references between beans. */ private boolean allowCircularReferences = true; @@ -144,8 +143,10 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac private final Set> ignoredDependencyTypes = new HashSet<>(); /** - * Dependency interfaces to ignore on dependency check and autowire, as Set of - * Class objects. By default, only the BeanFactory interface is ignored. + * Dependency interfaces to ignore on dependency check and autowire, as a Set + * of Class objects. + *

    By default, the {@code BeanNameAware}, {@code BeanFactoryAware}, and + * {@code BeanClassLoaderAware} interfaces are ignored. */ private final Set> ignoredDependencyInterfaces = new HashSet<>(); @@ -205,7 +206,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) { @@ -216,8 +217,7 @@ public void setParameterNameDiscoverer(@Nullable ParameterNameDiscoverer paramet * Return the ParameterNameDiscoverer to use for resolving method parameter * names if needed. */ - @Nullable - public ParameterNameDiscoverer getParameterNameDiscoverer() { + public @Nullable ParameterNameDiscoverer getParameterNameDiscoverer() { return this.parameterNameDiscoverer; } @@ -254,9 +254,8 @@ public boolean isAllowCircularReferences() { *

    This will only be used as a last resort in case of a circular reference * that cannot be resolved otherwise: essentially, preferring a raw instance * getting injected over a failure of the entire bean wiring process. - *

    Default is "false", as of Spring 2.0. Turn this on to allow for non-wrapped - * raw beans injected into some of your references, which was Spring 1.2's - * (arguably unclean) default behavior. + *

    Default is "false". Turn this on to allow for non-wrapped + * raw beans injected into some of your references. *

    NOTE: It is generally recommended to not rely on circular references * between your beans, in particular with auto-proxying involved. * @see #setAllowCircularReferences @@ -285,11 +284,15 @@ public void ignoreDependencyType(Class type) { /** * Ignore the given dependency interface for autowiring. *

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

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

    By default, the {@code BeanNameAware}, {@code BeanFactoryAware}, and + * {@code BeanClassLoaderAware} interfaces are ignored. * For further types to ignore, invoke this method for each type. + * @see org.springframework.beans.factory.BeanNameAware * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.beans.factory.BeanClassLoaderAware * @see org.springframework.context.ApplicationContextAware */ public void ignoreDependencyInterface(Class ifc) { @@ -359,7 +362,7 @@ public Object configureBean(Object existingBean, String beanName) throws BeansEx // Specialized methods for fine-grained control over the bean lifecycle //------------------------------------------------------------------------- - @Deprecated + @Deprecated(since = "6.1") @Override public Object createBean(Class beanClass, int autowireMode, boolean dependencyCheck) throws BeansException { // Use non-singleton bean definition, to avoid registering bean as dependent bean. @@ -467,8 +470,7 @@ public Object resolveBeanByName(String name, DependencyDescriptor descriptor) { } @Override - @Nullable - public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName) throws BeansException { + public @Nullable Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName) throws BeansException { return resolveDependency(descriptor, requestingBeanName, null, null); } @@ -483,7 +485,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()) { @@ -539,7 +541,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 +553,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. @@ -604,9 +606,7 @@ protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable if (ex instanceof BeanCreationException bce && beanName.equals(bce.getBeanName())) { throw bce; } - else { - throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex); - } + throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex); } if (earlySingletonExposure) { @@ -649,8 +649,7 @@ else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { } @Override - @Nullable - protected Class predictBeanType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { + protected @Nullable Class predictBeanType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { Class targetType = determineTargetType(beanName, mbd, typesToMatch); // Apply SmartInstantiationAwareBeanPostProcessors to predict the // eventual type after a before-instantiation shortcut. @@ -675,8 +674,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 +707,7 @@ protected Class determineTargetType(String beanName, RootBeanDefinition mbd, * @return the type for the bean if determinable, or {@code null} otherwise * @see #createBean */ - @Nullable - protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { + protected @Nullable Class getTypeForFactoryMethod(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { ResolvableType cachedReturnType = mbd.factoryMethodReturnType; if (cachedReturnType != null) { return cachedReturnType.resolve(); @@ -759,7 +756,7 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m // Fully resolve parameter names and argument values. ConstructorArgumentValues cav = mbd.getConstructorArgumentValues(); Class[] paramTypes = candidate.getParameterTypes(); - String[] paramNames = null; + @Nullable String[] paramNames = null; if (cav.containsNamedArgument()) { ParameterNameDiscoverer pnd = getParameterNameDiscoverer(); if (pnd != null) { @@ -767,7 +764,7 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m } } Set usedValueHolders = CollectionUtils.newHashSet(paramTypes.length); - Object[] args = new Object[paramTypes.length]; + @Nullable Object[] args = new Object[paramTypes.length]; for (int i = 0; i < args.length; i++) { ConstructorArgumentValues.ValueHolder valueHolder = cav.getArgumentValue( i, paramTypes[i], (paramNames != null ? paramNames[i] : null), usedValueHolders); @@ -814,10 +811,20 @@ 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; + } } /** @@ -834,10 +841,18 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m */ @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,56 +986,73 @@ 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) { - BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName); - if (bw != null) { - return (FactoryBean) bw.getWrappedInstance(); - } - Object beanInstance = getSingleton(beanName, false); - if (beanInstance instanceof FactoryBean factoryBean) { - return factoryBean; - } - if (isSingletonCurrentlyInCreation(beanName) || - (mbd.getFactoryBeanName() != null && isSingletonCurrentlyInCreation(mbd.getFactoryBeanName()))) { - return null; + private @Nullable FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) { + Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock(); + if (lockFlag == null) { + this.singletonLock.lock(); + } + else { + boolean locked = (lockFlag && this.singletonLock.tryLock()); + if (!locked) { + // Avoid shortcut FactoryBean instance but allow for subsequent type-based resolution. + resolveBeanClass(mbd, beanName); + return null; + } } - Object instance; try { - // Mark this bean as currently in creation, even if just partially. - beforeSingletonCreation(beanName); - // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. - instance = resolveBeforeInstantiation(beanName, mbd); - if (instance == null) { - bw = createBeanInstance(beanName, mbd, null); - instance = bw.getWrappedInstance(); - this.factoryBeanInstanceCache.put(beanName, bw); + BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName); + if (bw != null) { + return (FactoryBean) bw.getWrappedInstance(); } - } - catch (UnsatisfiedDependencyException ex) { - // Don't swallow, probably misconfiguration... - throw ex; - } - catch (BeanCreationException ex) { - // Don't swallow a linkage error since it contains a full stacktrace on - // first occurrence... and just a plain NoClassDefFoundError afterwards. - if (ex.contains(LinkageError.class)) { + Object beanInstance = getSingleton(beanName, false); + if (beanInstance instanceof FactoryBean factoryBean) { + return factoryBean; + } + if (isSingletonCurrentlyInCreation(beanName) || + (mbd.getFactoryBeanName() != null && isSingletonCurrentlyInCreation(mbd.getFactoryBeanName()))) { + return null; + } + + Object instance; + try { + // Mark this bean as currently in creation, even if just partially. + beforeSingletonCreation(beanName); + // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. + instance = resolveBeforeInstantiation(beanName, mbd); + if (instance == null) { + bw = createBeanInstance(beanName, mbd, null); + instance = bw.getWrappedInstance(); + this.factoryBeanInstanceCache.put(beanName, bw); + } + } + catch (UnsatisfiedDependencyException ex) { + // Don't swallow, probably misconfiguration... throw ex; } - // Instantiation failure, maybe too early... - if (logger.isDebugEnabled()) { - logger.debug("Bean creation exception on singleton FactoryBean type check: " + ex); + catch (BeanCreationException ex) { + // Don't swallow a linkage error since it contains a full stacktrace on + // first occurrence... and just a plain NoClassDefFoundError afterwards. + if (ex.contains(LinkageError.class)) { + throw ex; + } + // Instantiation failure, maybe too early... + if (logger.isDebugEnabled()) { + logger.debug("Bean creation exception on singleton FactoryBean type check: " + ex); + } + onSuppressedException(ex); + return null; } - onSuppressedException(ex); - return null; + finally { + // Finished partial creation of this bean. + afterSingletonCreation(beanName); + } + + return getFactoryBean(beanName, instance); } finally { - // Finished partial creation of this bean. - afterSingletonCreation(beanName); + this.singletonLock.unlock(); } - - return getFactoryBean(beanName, instance); } /** @@ -1031,8 +1063,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; } @@ -1090,8 +1121,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. @@ -1120,8 +1150,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) { @@ -1143,7 +1172,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); @@ -1215,8 +1244,8 @@ private BeanWrapper obtainFromSupplier(Supplier supplier, String beanName, Ro instance = obtainInstanceFromSupplier(supplier, beanName, mbd); } catch (Throwable ex) { - if (ex instanceof BeansException beansException) { - throw beansException; + if (ex instanceof BeanCreationException bce && beanName.equals(bce.getBeanName())) { + throw bce; } throw new BeanCreationException(beanName, "Instantiation of supplied bean failed", ex); } @@ -1245,8 +1274,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) { @@ -1263,15 +1291,15 @@ protected Object obtainInstanceFromSupplier(Supplier supplier, String beanNam * @see #obtainFromSupplier */ @Override - protected Object getObjectForBeanInstance( - Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) { + protected Object getObjectForBeanInstance(Object beanInstance, @Nullable Class requiredType, + String name, String beanName, @Nullable RootBeanDefinition mbd) { String currentlyCreatedBean = this.currentlyCreatedBean.get(); if (currentlyCreatedBean != null) { registerDependentBean(beanName, currentlyCreatedBean); } - return super.getObjectForBeanInstance(beanInstance, name, beanName, mbd); + return super.getObjectForBeanInstance(beanInstance, requiredType, name, beanName, mbd); } /** @@ -1283,8 +1311,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()) { @@ -1328,7 +1355,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); } @@ -1348,7 +1375,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); } @@ -1735,8 +1762,7 @@ private boolean isConvertibleProperty(String propertyName, BeanWrapper bw) { /** * Convert the given value for the specified target property. */ - @Nullable - private Object convertForProperty( + private @Nullable Object convertForProperty( @Nullable Object value, String propertyName, BeanWrapper bw, TypeConverter converter) { if (converter instanceof BeanWrapperImpl beanWrapper) { @@ -1769,6 +1795,11 @@ private Object convertForProperty( */ @SuppressWarnings("deprecation") protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) { + // Skip initialization of a NullBean + if (bean.getClass() == NullBean.class) { + return bean; + } + invokeAwareMethods(beanName, bean); Object wrappedBean = bean; @@ -1881,7 +1912,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); @@ -1950,8 +1981,7 @@ public CreateFromClassBeanDefinition(CreateFromClassBeanDefinition original) { } @Override - @Nullable - public Constructor[] getPreferredConstructors() { + public Constructor @Nullable [] getPreferredConstructors() { Constructor[] fromAttribute = super.getPreferredConstructors(); if (fromAttribute != null) { return fromAttribute; @@ -1978,8 +2008,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 450098ae7af2..93a64b5ca1e4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,8 @@ import java.util.Set; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataAttributeAccessor; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; @@ -32,7 +34,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.DescriptiveResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -94,10 +95,10 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess * Constant that indicates determining an appropriate autowire strategy * through introspection of the bean class. * @see #setAutowireMode - * @deprecated as of Spring 3.0: If you are using mixed autowiring strategies, - * use annotation-based autowiring for clearer demarcation of autowiring needs. + * @deprecated If you are using mixed autowiring strategies, use + * annotation-based autowiring for clearer demarcation of autowiring needs. */ - @Deprecated + @Deprecated(since = "3.0") public static final int AUTOWIRE_AUTODETECT = AutowireCapableBeanFactory.AUTOWIRE_AUTODETECT; /** @@ -165,25 +166,21 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess public static final String INFER_METHOD = "(inferred)"; - @Nullable - private volatile Object beanClass; + private volatile @Nullable Object beanClass; - @Nullable - private String scope = SCOPE_DEFAULT; + private @Nullable String scope = SCOPE_DEFAULT; private boolean abstractFlag = false; private boolean backgroundInit = false; - @Nullable - private Boolean lazyInit; + private @Nullable Boolean lazyInit; private int autowireMode = AUTOWIRE_NO; private int dependencyCheck = DEPENDENCY_CHECK_NONE; - @Nullable - private String[] dependsOn; + private String @Nullable [] dependsOn; private boolean autowireCandidate = true; @@ -195,32 +192,25 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private final Map qualifiers = new LinkedHashMap<>(); - @Nullable - private Supplier instanceSupplier; + private @Nullable Supplier instanceSupplier; private boolean nonPublicAccessAllowed = true; private boolean lenientConstructorResolution = true; - @Nullable - private String factoryBeanName; + private @Nullable String factoryBeanName; - @Nullable - private String factoryMethodName; + private @Nullable String factoryMethodName; - @Nullable - private ConstructorArgumentValues constructorArgumentValues; + private @Nullable ConstructorArgumentValues constructorArgumentValues; - @Nullable - private MutablePropertyValues propertyValues; + private @Nullable MutablePropertyValues propertyValues; private MethodOverrides methodOverrides = new MethodOverrides(); - @Nullable - private String[] initMethodNames; + private String @Nullable [] initMethodNames; - @Nullable - private String[] destroyMethodNames; + private String @Nullable [] destroyMethodNames; private boolean enforceInitMethod = true; @@ -230,11 +220,9 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private int role = BeanDefinition.ROLE_APPLICATION; - @Nullable - private String description; + private @Nullable String description; - @Nullable - private Resource resource; + private @Nullable Resource resource; /** @@ -429,8 +417,7 @@ public void setBeanClassName(@Nullable String beanClassName) { * @see #getBeanClass() */ @Override - @Nullable - public String getBeanClassName() { + public @Nullable String getBeanClassName() { Object beanClassObject = this.beanClass; // defensive access to volatile beanClass field return (beanClassObject instanceof Class clazz ? clazz.getName() : (String) beanClassObject); } @@ -494,8 +481,7 @@ public boolean hasBeanClass() { * @return the resolved bean class * @throws ClassNotFoundException if the class name could be resolved */ - @Nullable - public Class resolveBeanClass(@Nullable ClassLoader classLoader) throws ClassNotFoundException { + public @Nullable Class resolveBeanClass(@Nullable ClassLoader classLoader) throws ClassNotFoundException { String className = getBeanClassName(); if (className == null) { return null; @@ -534,8 +520,7 @@ public void setScope(@Nullable String scope) { *

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

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

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

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

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

    The default is no description. */ @Override - @Nullable - public String getDescription() { + public @Nullable String getDescription() { return this.description; } @@ -1217,8 +1191,7 @@ public void setResource(@Nullable Resource resource) { /** * Return the resource that this bean definition came from. */ - @Nullable - public Resource getResource() { + public @Nullable Resource getResource() { return this.resource; } @@ -1235,13 +1208,12 @@ public void setResourceDescription(@Nullable String resourceDescription) { * @see #setResourceDescription */ @Override - @Nullable - public String getResourceDescription() { + public @Nullable String getResourceDescription() { return (this.resource != null ? this.resource.getDescription() : null); } /** - * 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); @@ -1252,8 +1224,7 @@ public void setOriginatingBeanDefinition(BeanDefinition originatingBd) { * @see #setOriginatingBeanDefinition */ @Override - @Nullable - public BeanDefinition getOriginatingBeanDefinition() { + public @Nullable BeanDefinition getOriginatingBeanDefinition() { return (this.resource instanceof BeanDefinitionResource bdr ? bdr.getBeanDefinition() : null); } @@ -1383,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); @@ -1392,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..1a62bf241446 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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 32af62487c5e..1f8c483db66b 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.beans.PropertyEditor; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -31,6 +32,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; @@ -64,12 +67,12 @@ import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; import org.springframework.core.DecoratingClassLoader; import org.springframework.core.NamedThreadLocal; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.log.LogMessage; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.StartupStep; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -115,27 +118,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); @@ -144,23 +145,21 @@ 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. **/ + /** Application startup metrics. */ private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; /** Map from bean name to merged RootBeanDefinition. */ @@ -205,7 +204,18 @@ public T getBean(String name, Class requiredType) throws BeansException { } @Override - public Object getBean(String name, Object... args) throws BeansException { + @SuppressWarnings("unchecked") + public T getBean(String name, ParameterizedTypeReference typeReference) throws BeansException { + Object bean = getBean(name); + Type requiredType = typeReference.getType(); + if (!ResolvableType.forType(requiredType).isInstance(bean)) { + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + return (T) bean; + } + + @Override + public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException { return doGetBean(name, null, args, false); } @@ -218,7 +228,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 +247,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); @@ -255,7 +265,7 @@ protected T doGetBean( logger.trace("Returning cached instance of singleton bean '" + beanName + "'"); } } - beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null); + beanInstance = getObjectForBeanInstance(sharedInstance, requiredType, name, beanName, null); } else { @@ -343,7 +353,7 @@ else if (requiredType != null) { throw ex; } }); - beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + beanInstance = getObjectForBeanInstance(sharedInstance, requiredType, name, beanName, mbd); } else if (mbd.isPrototype()) { @@ -356,7 +366,7 @@ else if (mbd.isPrototype()) { finally { afterPrototypeCreation(beanName); } - beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + beanInstance = getObjectForBeanInstance(prototypeInstance, requiredType, name, beanName, mbd); } else { @@ -378,7 +388,7 @@ else if (mbd.isPrototype()) { afterPrototypeCreation(beanName); } }); - beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + beanInstance = getObjectForBeanInstance(scopedInstance, requiredType, name, beanName, mbd); } catch (IllegalStateException ex) { throw new ScopeNotActiveException(beanName, scopeName, ex); @@ -416,7 +426,7 @@ T adaptBeanInstance(String name, Object bean, @Nullable Class requiredTyp catch (TypeMismatchException ex) { if (logger.isTraceEnabled()) { logger.trace("Failed to convert bean '" + name + "' to required type '" + - ClassUtils.getQualifiedName(requiredType) + "'", ex); + requiredType.getTypeName() + "'", ex); } throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } @@ -517,8 +527,7 @@ public boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuc * to check whether the bean with the given name matches the specified type. Allow * additional constraints to be applied to ensure that beans are not created early. * @param name the name of the bean to query - * @param typeToMatch the type to match against (as a - * {@code ResolvableType}) + * @param typeToMatch the type to match against (as a {@code ResolvableType}) * @return {@code true} if the bean type matches, {@code false} if it * doesn't match or cannot be determined yet * @throws NoSuchBeanDefinitionException if there is no bean with the given name @@ -539,6 +548,11 @@ protected boolean isTypeMatch(String name, ResolvableType typeToMatch, boolean a // Determine target for FactoryBean match if necessary. if (beanInstance instanceof FactoryBean factoryBean) { if (!isFactoryDereference) { + Class classToMatch = typeToMatch.resolve(); + if (factoryBean instanceof SmartFactoryBean smartFactoryBean && + classToMatch != null && smartFactoryBean.supportsType(classToMatch)) { + return true; + } Class type = getTypeForFactoryBean(factoryBean); if (type == null) { return false; @@ -557,7 +571,6 @@ else if (typeToMatch.hasGenerics() && containsBeanDefinition(beanName)) { } 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()); @@ -703,14 +716,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. @@ -767,16 +778,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)) { @@ -798,8 +809,7 @@ public String[] getAliases(String name) { //--------------------------------------------------------------------- @Override - @Nullable - public BeanFactory getParentBeanFactory() { + public @Nullable BeanFactory getParentBeanFactory() { return this.parentBeanFactory; } @@ -832,8 +842,7 @@ public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { } @Override - @Nullable - public ClassLoader getBeanClassLoader() { + public @Nullable ClassLoader getBeanClassLoader() { return this.beanClassLoader; } @@ -843,8 +852,7 @@ public void setTempClassLoader(@Nullable ClassLoader tempClassLoader) { } @Override - @Nullable - public ClassLoader getTempClassLoader() { + public @Nullable ClassLoader getTempClassLoader() { return this.tempClassLoader; } @@ -864,8 +872,7 @@ public void setBeanExpressionResolver(@Nullable BeanExpressionResolver resolver) } @Override - @Nullable - public BeanExpressionResolver getBeanExpressionResolver() { + public @Nullable BeanExpressionResolver getBeanExpressionResolver() { return this.beanExpressionResolver; } @@ -875,15 +882,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); + } } /** @@ -921,8 +932,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; } @@ -953,8 +963,7 @@ public boolean hasEmbeddedValueResolver() { } @Override - @Nullable - public String resolveEmbeddedValue(@Nullable String value) { + public @Nullable String resolveEmbeddedValue(@Nullable String value) { if (value == null) { return null; } @@ -1089,8 +1098,7 @@ public String[] getRegisteredScopeNames() { } @Override - @Nullable - public Scope getRegisteredScope(String scopeName) { + public @Nullable Scope getRegisteredScope(String scopeName) { Assert.notNull(scopeName, "Scope identifier must not be null"); return this.scopes.get(scopeName); } @@ -1114,6 +1122,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; @@ -1144,7 +1153,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. @@ -1283,7 +1292,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; @@ -1313,29 +1322,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) -> @@ -1343,6 +1341,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 @@ -1453,7 +1474,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) { @@ -1482,6 +1503,18 @@ private void copyRelevantMergedBeanDefinitionCaches(RootBeanDefinition previous, } } + /** + * Cache the given merged bean definition. + *

    Subclasses can override this to derive additional cached state + * from the final post-processed bean definition. + * @param mbd the merged bean definition to cache + * @param beanName the name of the bean + * @since 6.2.6 + */ + protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) { + this.mergedBeanDefinitions.put(beanName, mbd); + } + /** * Check the given merged bean definition, * potentially throwing validation exceptions. @@ -1489,7 +1522,7 @@ private void copyRelevantMergedBeanDefinitionCaches(RootBeanDefinition previous, * @param beanName the name of the bean * @param args the arguments for bean creation, if any */ - protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) { + protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object @Nullable [] args) { if (mbd.isAbstract()) { throw new BeanIsAbstractException(beanName); } @@ -1511,7 +1544,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 */ @@ -1534,8 +1567,7 @@ public void clearMetadataCache() { * @return the resolved bean class (or {@code null} if none) * @throws CannotLoadBeanClassException if we failed to load the class */ - @Nullable - protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Class... typesToMatch) + protected @Nullable Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Class... typesToMatch) throws CannotLoadBeanClassException { try { @@ -1560,8 +1592,7 @@ protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Cla } } - @Nullable - private Class doResolveBeanClass(RootBeanDefinition mbd, Class... typesToMatch) + private @Nullable Class doResolveBeanClass(RootBeanDefinition mbd, Class... typesToMatch) throws ClassNotFoundException { ClassLoader beanClassLoader = getBeanClassLoader(); @@ -1570,7 +1601,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; @@ -1628,8 +1659,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; } @@ -1660,8 +1690,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; @@ -1712,9 +1741,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()) { @@ -1816,8 +1851,8 @@ protected boolean hasBeanCreationStarted() { * @param mbd the merged bean definition * @return the object to expose for the bean */ - protected Object getObjectForBeanInstance( - Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) { + protected Object getObjectForBeanInstance(Object beanInstance, @Nullable Class requiredType, + String name, String beanName, @Nullable RootBeanDefinition mbd) { // Don't let calling code try to dereference the factory if the bean isn't a factory. if (BeanFactoryUtils.isFactoryDereference(name)) { @@ -1854,7 +1889,7 @@ protected Object getObjectForBeanInstance( mbd = getMergedLocalBeanDefinition(beanName); } boolean synthetic = (mbd != null && mbd.isSynthetic()); - object = getObjectFromFactoryBean(factoryBean, beanName, !synthetic); + object = getObjectFromFactoryBean(factoryBean, requiredType, beanName, !synthetic); } return object; } @@ -1972,7 +2007,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; @@ -2089,4 +2124,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/AutowireCandidateQualifier.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateQualifier.java index 405e35c7eb60..498980413982 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateQualifier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateQualifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 d69daa2e4118..3a5cdb51555a 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-2024 the original author or authors. + * Copyright 2002-present the original author 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() */ @@ -79,8 +80,7 @@ default boolean hasQualifier(DependencyDescriptor descriptor) { * @return the qualifier value, if any * @since 6.2 */ - @Nullable - default String getSuggestedName(DependencyDescriptor descriptor) { + default @Nullable String getSuggestedName(DependencyDescriptor descriptor) { return null; } @@ -92,8 +92,7 @@ default String getSuggestedName(DependencyDescriptor descriptor) { * or {@code null} if none found * @since 3.0 */ - @Nullable - default Object getSuggestedValue(DependencyDescriptor descriptor) { + default @Nullable Object getSuggestedValue(DependencyDescriptor descriptor) { return null; } @@ -107,8 +106,7 @@ default Object getSuggestedValue(DependencyDescriptor descriptor) { * or {@code null} if straight resolution is to be performed * @since 4.0 */ - @Nullable - default Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { + default @Nullable Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { return null; } @@ -121,8 +119,7 @@ default Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor * @return the lazy resolution proxy class for the dependency target, if any * @since 6.0 */ - @Nullable - default Class getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) { + default @Nullable Class getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java index 6baa1fd13880..475e0984617a 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-present the original author 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 d82d66bd75c0..d39b7bfe1769 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java index eb76dd9d13d8..428e0ab505f0 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-2024 the original author or authors. + * Copyright 2002-present the original author 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; /** @@ -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..11d00c38c89c 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-present the original author 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..b050b9b89a54 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package org.springframework.beans.factory.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; /** * Simple interface for bean definition readers that specifies load methods with @@ -29,10 +30,6 @@ * load and register methods for bean definitions, specific to * their bean definition format. * - *

    Note that a bean definition reader does not have to implement - * this interface. It only serves as a suggestion for bean definition - * readers that want to follow standard naming conventions. - * * @author Juergen Hoeller * @since 1.1 * @see org.springframework.core.io.Resource @@ -63,8 +60,7 @@ public interface BeanDefinitionReader { * @see #loadBeanDefinitions(String) * @see org.springframework.core.io.support.ResourcePatternResolver */ - @Nullable - ResourceLoader getResourceLoader(); + @Nullable ResourceLoader getResourceLoader(); /** * Return the class loader to use for bean classes. @@ -72,8 +68,7 @@ public interface BeanDefinitionReader { * but rather to just register bean definitions with class names, * with the corresponding classes to be resolved later (or never). */ - @Nullable - ClassLoader getBeanClassLoader(); + @Nullable ClassLoader getBeanClassLoader(); /** * Return the {@link BeanNameGenerator} to use for anonymous beans diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java index b02687792e15..a8e568b7237f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author 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.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; @@ -32,7 +33,6 @@ * @author Juergen Hoeller * @author Rob Harrop * @since 1.1 - * @see PropertiesBeanDefinitionReader * @see org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader */ public abstract class BeanDefinitionReaderUtils { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistry.java index f5368ea9e486..4381cf7e61a8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,6 @@ * @see DefaultListableBeanFactory * @see org.springframework.context.support.GenericApplicationContext * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader - * @see PropertiesBeanDefinitionReader */ public interface BeanDefinitionRegistry extends AliasRegistry { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistryPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistryPostProcessor.java index 13763ee337ac..741fe5859491 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistryPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..d6116b116433 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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/BeanDefinitionValidationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValidationException.java index 88e0458b8ef2..c8aaf00e196b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValidationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValidationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java index c3efcdcc0b2d..6df80c586e33 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,11 +329,11 @@ 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(); + String resolvedName = String.valueOf(doEvaluate(ref.getBeanName())); if (ref.isToParent()) { BeanFactory parent = this.beanFactory.getParentBeanFactory(); if (parent == null) { @@ -347,21 +343,25 @@ private Object resolveReference(Object argName, RuntimeBeanReference ref) { " in parent factory: no parent factory available"); } if (beanType != null) { - bean = parent.getBean(beanType); + bean = (parent.containsBean(resolvedName) ? + parent.getBean(resolvedName, beanType) : parent.getBean(beanType)); } else { - bean = parent.getBean(String.valueOf(doEvaluate(ref.getBeanName()))); + bean = parent.getBean(resolvedName); } } else { - String resolvedName; if (beanType != null) { - NamedBeanHolder namedBean = this.beanFactory.resolveNamedBean(beanType); - bean = namedBean.getBeanInstance(); - resolvedName = namedBean.getBeanName(); + if (this.beanFactory.containsBean(resolvedName)) { + bean = this.beanFactory.getBean(resolvedName, beanType); + } + else { + NamedBeanHolder namedBean = this.beanFactory.resolveNamedBean(beanType); + bean = namedBean.getBeanInstance(); + resolvedName = namedBean.getBeanName(); + } } else { - resolvedName = String.valueOf(doEvaluate(ref.getBeanName())); bean = this.beanFactory.getBean(resolvedName); } this.beanFactory.registerDependentBean(resolvedName, this.beanName); @@ -385,8 +385,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. @@ -407,7 +406,8 @@ private Object resolveInnerBeanValue(Object argName, String innerBeanName, RootB Object innerBean = this.beanFactory.createBean(actualInnerBeanName, mbd, null); if (innerBean instanceof FactoryBean factoryBean) { boolean synthetic = mbd.isSynthetic(); - innerBean = this.beanFactory.getObjectFromFactoryBean(factoryBean, actualInnerBeanName, !synthetic); + innerBean = this.beanFactory.getObjectFromFactoryBean( + factoryBean, null, actualInnerBeanName, !synthetic); } if (innerBean instanceof NullBean) { innerBean = null; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanNameGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanNameGenerator.java index d7d3c9b35d72..aea04016e6eb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanNameGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanNameGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author 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 @@ * * @author Juergen Hoeller * @since 2.0.3 + * @see org.springframework.context.annotation.ConfigurationBeanNameGenerator */ public interface BeanNameGenerator { 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..cfc3135ce375 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java @@ -0,0 +1,349 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.Objects; +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.BeanFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +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 register(BeanRegistrar registrar) { + Assert.notNull(registrar, "BeanRegistrar must not be null"); + registrar.register(this, this.environment); + } + + @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(ParameterizedTypeReference beanType) { + ResolvableType resolvableType = ResolvableType.forType(beanType); + String beanName = BeanDefinitionReaderUtils.uniqueBeanName(Objects.requireNonNull(resolvableType.resolve()).getName(), this.beanRegistry); + registerBean(beanName, beanType); + 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 String registerBean(ParameterizedTypeReference beanType, Consumer> customizer) { + ResolvableType resolvableType = ResolvableType.forType(beanType); + Class beanClass = Objects.requireNonNull(resolvableType.resolve()); + String beanName = BeanDefinitionReaderUtils.uniqueBeanName(beanClass.getName(), this.beanRegistry); + registerBean(beanName, beanType, 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, ParameterizedTypeReference beanType) { + ResolvableType resolvableType = ResolvableType.forType(beanType); + Class beanClass = Objects.requireNonNull(resolvableType.resolve()); + BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); + beanDefinition.setTargetType(resolvableType); + 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> customizer) { + BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); + customizer.accept(new BeanSpecAdapter<>(beanDefinition, this.beanFactory)); + if (this.customizers != null && this.customizers.containsKey(name)) { + for (BeanDefinitionCustomizer registryCustomizer : this.customizers.get(name)) { + registryCustomizer.customize(beanDefinition); + } + } + this.beanRegistry.registerBeanDefinition(name, beanDefinition); + } + + @Override + public void registerBean(String name, ParameterizedTypeReference beanType, Consumer> customizer) { + ResolvableType resolvableType = ResolvableType.forType(beanType); + Class beanClass = Objects.requireNonNull(resolvableType.resolve()); + BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); + beanDefinition.setTargetType(resolvableType); + customizer.accept(new BeanSpecAdapter<>(beanDefinition, this.beanFactory)); + if (this.customizers != null && this.customizers.containsKey(name)) { + for (BeanDefinitionCustomizer registryCustomizer : this.customizers.get(name)) { + registryCustomizer.customize(beanDefinition); + } + } + this.beanRegistry.registerBeanDefinition(name, beanDefinition); + } + + @Override + public boolean containsBean(String name) { + return this.beanFactory.containsBean(name); + } + + @Override + public boolean containsBean(Class beanType) { + return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanType).length > 0; + } + + @Override + public boolean containsBean(ParameterizedTypeReference beanType) { + ResolvableType resolvableType = ResolvableType.forType(beanType); + return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, resolvableType).length > 0; + } + + + /** + * {@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 BeanFactory beanFactory; + + public BeanSpecAdapter(RootBeanDefinition beanDefinition, BeanFactory 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 scope(String scope) { + this.beanDefinition.setScope(scope); + return this; + } + + @Override + public Spec supplier(Function supplier) { + this.beanDefinition.setInstanceSupplier(() -> + supplier.apply(new SupplierContextAdapter(this.beanFactory))); + return this; + } + } + + + private static class SupplierContextAdapter implements SupplierContext { + + private final BeanFactory beanFactory; + + public SupplierContextAdapter(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public T bean(Class beanClass) throws BeansException { + return this.beanFactory.getBean(beanClass); + } + + @Override + public T bean(ParameterizedTypeReference beanType) throws BeansException { + return this.beanFactory.getBeanProvider(beanType).getObject(); + } + + @Override + public T bean(String name, Class beanClass) throws BeansException { + return this.beanFactory.getBean(name, beanClass); + } + + @Override + public ObjectProvider beanProvider(Class beanClass) { + return this.beanFactory.getBeanProvider(beanClass); + } + + @Override + public ObjectProvider beanProvider(ParameterizedTypeReference beanType) { + return this.beanFactory.getBeanProvider(beanType); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java index 4c1f826f56a0..ce63833d5e16 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; +import org.springframework.aot.AotDetector; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; @@ -36,7 +38,6 @@ import org.springframework.cglib.proxy.MethodProxy; import org.springframework.cglib.proxy.NoOp; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -153,7 +154,7 @@ public Class createEnhancedSubclass(RootBeanDefinition beanDefinition) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(beanDefinition.getBeanClass()); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts()); if (this.owner instanceof ConfigurableBeanFactory cbf) { ClassLoader cl = cbf.getBeanClassLoader(); enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl)); @@ -241,8 +242,7 @@ public LookupOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanFa } @Override - @Nullable - public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { + public @Nullable Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { // Cast is safe, as CallbackFilter filters are used selectively. LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method); Assert.state(lo != null, "LookupOverride not found"); @@ -276,18 +276,15 @@ public ReplaceOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanF this.owner = owner; } - @Nullable @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { + public @Nullable Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { ReplaceOverride ro = (ReplaceOverride) getBeanDefinition().getMethodOverrides().getOverride(method); Assert.state(ro != null, "ReplaceOverride not found"); - // TODO could cache if a singleton for minor performance optimization MethodReplacer mr = this.owner.getBean(ro.getMethodReplacerBeanName(), MethodReplacer.class); return processReturnType(method, mr.reimplement(obj, method, args)); } - @Nullable - private T processReturnType(Method method, @Nullable T returnValue) { + private @Nullable T processReturnType(Method method, @Nullable T returnValue) { Class returnType = method.getReturnType(); if (returnValue == null && returnType != void.class && returnType.isPrimitive()) { throw new IllegalStateException( diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java index 5f15616d95a1..4dc5f6cf88ee 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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 96773ccfee5c..3bc40b3d8b5b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import java.util.function.Supplier; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.BeanUtils; @@ -68,7 +69,6 @@ import org.springframework.core.NamedThreadLocal; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -131,16 +131,16 @@ public ConstructorResolver(AbstractAutowireCapableBeanFactory beanFactory) { * or {@code null} if none (-> use constructor argument values from bean definition) * @return a BeanWrapper for the new instance */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd, - @Nullable Constructor[] chosenCtors, @Nullable Object[] explicitArgs) { + Constructor @Nullable [] chosenCtors, @Nullable Object @Nullable [] explicitArgs) { BeanWrapperImpl bw = new BeanWrapperImpl(); this.beanFactory.initBeanWrapper(bw); Constructor constructorToUse = null; ArgumentsHolder argsHolderToUse = null; - Object[] argsToUse = null; + @Nullable Object[] argsToUse = null; if (explicitArgs != null) { argsToUse = explicitArgs; @@ -227,7 +227,7 @@ public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd, Class[] paramTypes = candidate.getParameterTypes(); if (resolvedValues != null) { try { - String[] paramNames = null; + @Nullable String[] paramNames = null; if (resolvedValues.containsNamedArgument()) { paramNames = ConstructorPropertiesChecker.evaluate(candidate, parameterCount); if (paramNames == null) { @@ -393,9 +393,9 @@ private boolean isStaticCandidate(Method method, Class factoryClass) { * method, or {@code null} if none (-> use constructor argument values from bean definition) * @return a BeanWrapper for the new instance */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public BeanWrapper instantiateUsingFactoryMethod( - String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) { + String beanName, RootBeanDefinition mbd, @Nullable Object @Nullable [] explicitArgs) { BeanWrapperImpl bw = new BeanWrapperImpl(); this.beanFactory.initBeanWrapper(bw); @@ -431,13 +431,13 @@ public BeanWrapper instantiateUsingFactoryMethod( Method factoryMethodToUse = null; ArgumentsHolder argsHolderToUse = null; - Object[] argsToUse = null; + @Nullable Object[] argsToUse = null; if (explicitArgs != null) { argsToUse = explicitArgs; } else { - Object[] argsToResolve = null; + @Nullable Object[] argsToResolve = null; synchronized (mbd.constructorArgumentLock) { factoryMethodToUse = (Method) mbd.resolvedConstructorOrFactoryMethod; if (factoryMethodToUse != null && mbd.constructorArgumentsResolved) { @@ -536,7 +536,7 @@ public BeanWrapper instantiateUsingFactoryMethod( else { // Resolved constructor arguments: type conversion and/or autowiring necessary. try { - String[] paramNames = null; + @Nullable String[] paramNames = null; if (resolvedValues != null && resolvedValues.containsNamedArgument()) { ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer(); if (pnd != null) { @@ -624,7 +624,7 @@ else if (void.class == factoryMethodToUse.getReturnType()) { "Invalid factory method '" + mbd.getFactoryMethodName() + "' on class [" + factoryClass.getName() + "]: needs to have a non-void return type!"); } - else if (KotlinDetector.isKotlinPresent() && KotlinDetector.isSuspendingFunction(factoryMethodToUse)) { + else if (KotlinDetector.isSuspendingFunction(factoryMethodToUse)) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid factory method '" + mbd.getFactoryMethodName() + "' on class [" + factoryClass.getName() + "]: suspending functions are not supported!"); @@ -647,7 +647,7 @@ else if (ambiguousFactoryMethods != null) { } private Object instantiate(String beanName, RootBeanDefinition mbd, - @Nullable Object factoryBean, Method factoryMethod, Object[] args) { + @Nullable Object factoryBean, Method factoryMethod, @Nullable Object[] args) { try { return this.beanFactory.getInstantiationStrategy().instantiate( @@ -719,7 +719,7 @@ private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd, */ private ArgumentsHolder createArgumentArray( String beanName, RootBeanDefinition mbd, @Nullable ConstructorArgumentValues resolvedValues, - BeanWrapper bw, Class[] paramTypes, @Nullable String[] paramNames, Executable executable, + BeanWrapper bw, Class[] paramTypes, @Nullable String @Nullable [] paramNames, Executable executable, boolean autowiring, boolean fallback) throws UnsatisfiedDependencyException { TypeConverter customConverter = this.beanFactory.getCustomTypeConverter(); @@ -814,7 +814,7 @@ private ArgumentsHolder createArgumentArray( /** * Resolve the prepared arguments stored in the given bean definition. */ - private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw, + private @Nullable Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw, Executable executable, Object[] argsToResolve) { TypeConverter customConverter = this.beanFactory.getCustomTypeConverter(); @@ -823,7 +823,7 @@ private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mb new BeanDefinitionValueResolver(this.beanFactory, beanName, mbd, converter); Class[] paramTypes = executable.getParameterTypes(); - Object[] resolvedArgs = new Object[argsToResolve.length]; + @Nullable Object[] resolvedArgs = new Object[argsToResolve.length]; for (int argIndex = 0; argIndex < argsToResolve.length; argIndex++) { Object argValue = argsToResolve[argIndex]; Class paramType = paramTypes[argIndex]; @@ -897,8 +897,7 @@ private Constructor getUserDeclaredConstructor(Constructor constructor) { /** * Resolve the specified argument which is supposed to be autowired. */ - @Nullable - Object resolveAutowiredArgument(DependencyDescriptor descriptor, Class paramType, String beanName, + @Nullable Object resolveAutowiredArgument(DependencyDescriptor descriptor, Class paramType, String beanName, @Nullable Set autowiredBeanNames, TypeConverter typeConverter, boolean fallback) { if (InjectionPoint.class.isAssignableFrom(paramType)) { @@ -918,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); } @@ -1041,8 +1040,7 @@ private ResolvableType determineParameterValueType(RootBeanDefinition mbd, Value return ResolvableType.forInstance(value); } - @Nullable - private Constructor resolveConstructor(String beanName, RootBeanDefinition mbd, + private @Nullable Constructor resolveConstructor(String beanName, RootBeanDefinition mbd, Supplier beanType, List valueTypes) { Class type = ClassUtils.getUserClass(beanType.get().toClass()); @@ -1089,8 +1087,7 @@ private Constructor resolveConstructor(String beanName, RootBeanDefinition mb return (typeConversionFallbackMatches.size() == 1 ? typeConversionFallbackMatches.get(0) : null); } - @Nullable - private Method resolveFactoryMethod(String beanName, RootBeanDefinition mbd, List valueTypes) { + private @Nullable Method resolveFactoryMethod(String beanName, RootBeanDefinition mbd, List valueTypes) { if (mbd.isFactoryMethodUnique) { Method resolvedFactoryMethod = mbd.getResolvedFactoryMethod(); if (resolvedFactoryMethod != null) { @@ -1149,8 +1146,7 @@ else if (candidates.size() > 1) { return null; } - @Nullable - private Method resolveFactoryMethod(List executables, + private @Nullable Method resolveFactoryMethod(List executables, Function> parameterTypesFactory, List valueTypes) { @@ -1241,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 @@ -1257,8 +1253,7 @@ private Predicate isSimpleValueType(ResolvableType valueType) { BeanUtils.isSimpleValueType(valueType.toClass())); } - @Nullable - private Class getFactoryBeanClass(String beanName, RootBeanDefinition mbd) { + private @Nullable Class getFactoryBeanClass(String beanName, RootBeanDefinition mbd) { Class beanClass = this.beanFactory.resolveBeanClass(mbd, beanName); return (beanClass != null && FactoryBean.class.isAssignableFrom(beanClass) ? beanClass : null); } @@ -1288,8 +1283,7 @@ static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectio * This variant adds a lenient fallback to the default constructor if available, similar to * {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors}. */ - @Nullable - static Constructor[] determinePreferredConstructors(Class clazz) { + static Constructor @Nullable [] determinePreferredConstructors(Class clazz) { Constructor primaryCtor = BeanUtils.findPrimaryConstructor(clazz); Constructor defaultCtor; @@ -1323,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; } } @@ -1337,11 +1331,11 @@ else if (ctors.length == 0) { */ private static class ArgumentsHolder { - public final Object[] rawArguments; + public final @Nullable Object[] rawArguments; - public final Object[] arguments; + public final @Nullable Object[] arguments; - public final Object[] preparedArguments; + public final @Nullable Object[] preparedArguments; public boolean resolveNecessary = false; @@ -1351,7 +1345,7 @@ public ArgumentsHolder(int size) { this.preparedArguments = new Object[size]; } - public ArgumentsHolder(Object[] args) { + public ArgumentsHolder(@Nullable Object[] args) { this.rawArguments = args; this.arguments = args; this.preparedArguments = args; @@ -1401,8 +1395,7 @@ public void storeCache(RootBeanDefinition mbd, Executable constructorOrFactoryMe */ private static class ConstructorPropertiesChecker { - @Nullable - public static String[] evaluate(Constructor candidate, int paramCount) { + public static String @Nullable [] evaluate(Constructor candidate, int paramCount) { ConstructorProperties cp = candidate.getAnnotation(ConstructorProperties.class); if (cp != null) { String[] names = cp.value(); @@ -1427,8 +1420,7 @@ public static String[] evaluate(Constructor candidate, int paramCount) { @SuppressWarnings("serial") private static class ConstructorDependencyDescriptor extends DependencyDescriptor { - @Nullable - private volatile String shortcut; + private volatile @Nullable String shortcut; public ConstructorDependencyDescriptor(MethodParameter methodParameter, boolean required) { super(methodParameter, required); @@ -1443,8 +1435,7 @@ public boolean hasShortcut() { } @Override - @Nullable - public Object resolveShortcut(BeanFactory beanFactory) { + public @Nullable Object resolveShortcut(BeanFactory beanFactory) { String shortcut = this.shortcut; return (shortcut != null ? beanFactory.getBean(shortcut, getDependencyType()) : null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultBeanNameGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultBeanNameGenerator.java index 9632f5a7115b..cef6ff9fa8ca 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultBeanNameGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultBeanNameGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 6d8c27708bfb..a2a57bcb8191 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.IdentityHashMap; import java.util.Iterator; @@ -47,6 +48,7 @@ import java.util.stream.Stream; import jakarta.inject.Provider; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; @@ -58,6 +60,7 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.CannotLoadBeanClassException; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; @@ -75,14 +78,15 @@ import org.springframework.core.NamedThreadLocal; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; +import org.springframework.core.SpringProperties; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.log.LogMessage; import org.springframework.core.metrics.StartupStep; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -128,17 +132,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; } } @@ -147,23 +164,22 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private static final Map> serializableFactories = new ConcurrentHashMap<>(8); + /** Whether strict locking is enforced or relaxed in this factory. */ + private final @Nullable Boolean strictLocking = SpringProperties.checkFlag(STRICT_LOCKING_PROPERTY_NAME); + /** Optional id for this factory, for serialization purposes. */ - @Nullable - private String serializationId; + private @Nullable String serializationId; /** Whether to allow re-registration of a different definition with the same name. */ - @Nullable - private Boolean allowBeanDefinitionOverriding; + private @Nullable Boolean allowBeanDefinitionOverriding; /** Whether to allow eager class loading even for lazy-init beans. */ private boolean allowEagerClassLoading = true; - @Nullable - private Executor bootstrapExecutor; + private @Nullable Executor bootstrapExecutor; /** Optional OrderComparator for dependency Lists and arrays. */ - @Nullable - private Comparator dependencyComparator; + private @Nullable Comparator dependencyComparator; /** Resolver to use for checking if a bean definition is an autowire candidate. */ private AutowireCandidateResolver autowireCandidateResolver = SimpleAutowireCandidateResolver.INSTANCE; @@ -177,8 +193,8 @@ 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 bean definition names with a primary marker plus corresponding type. */ + private final Map> primaryBeanNamesWithType = new ConcurrentHashMap<>(16); /** Map of singleton and non-singleton bean names, keyed by dependency type. */ private final Map, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64); @@ -193,12 +209,14 @@ 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"); @@ -238,8 +256,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; } @@ -292,8 +309,7 @@ public void setBootstrapExecutor(@Nullable Executor bootstrapExecutor) { } @Override - @Nullable - public Executor getBootstrapExecutor() { + public @Nullable Executor getBootstrapExecutor() { return this.bootstrapExecutor; } @@ -311,8 +327,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; } @@ -347,7 +362,7 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { 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); } } @@ -364,7 +379,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) { @@ -384,6 +399,10 @@ public ObjectProvider getBeanProvider(ResolvableType requiredType) { return getBeanProvider(requiredType, true); } + public ObjectProvider getBeanProvider(ParameterizedTypeReference requiredType) { + return getBeanProvider(ResolvableType.forType(requiredType), true); + } + //--------------------------------------------------------------------- // Implementation of ListableBeanFactory interface @@ -429,7 +448,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); @@ -437,8 +456,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); } @@ -460,8 +478,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); } @@ -485,20 +502,20 @@ public void ifUnique(Consumer dependencyConsumer) throws BeansException { @SuppressWarnings("unchecked") @Override public Stream stream() { - return Arrays.stream(getBeanNamesForTypedStream(requiredType, allowEagerInit)) - .map(name -> (T) getBean(name)) + return Arrays.stream(beanNamesForStream(requiredType, true, allowEagerInit)) + .map(name -> (T) resolveBean(name, requiredType)) .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(); } Map matchingBeans = CollectionUtils.newLinkedHashMap(beanNames.length); for (String beanName : beanNames) { - Object beanInstance = getBean(beanName); + Object beanInstance = resolveBean(beanName, requiredType); if (!(beanInstance instanceof NullBean)) { matchingBeans.put(beanName, (T) beanInstance); } @@ -506,18 +523,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) resolveBean(name, requiredType)) + .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 = resolveBean(beanName, requiredType); + 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); @@ -531,8 +573,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 @@ -598,10 +640,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; @@ -690,11 +737,14 @@ public Map getBeansOfType( Map result = CollectionUtils.newLinkedHashMap(beanNames.length); for (String beanName : beanNames) { try { - Object beanInstance = getBean(beanName); + Object beanInstance = (type != null ? getBean(beanName, type) : getBean(beanName)); if (!(beanInstance instanceof NullBean)) { result.put(beanName, (T) beanInstance); } } + catch (BeanNotOfRequiredTypeException ex) { + // Ignore - probably a NullBean + } catch (BeanCreationException ex) { Throwable rootCause = ex.getMostSpecificCause(); if (rootCause instanceof BeanCurrentlyInCreationException bce) { @@ -747,16 +797,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 { @@ -770,7 +818,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) { @@ -809,7 +857,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) { @@ -867,7 +915,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); } @@ -901,7 +949,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); @@ -969,8 +1017,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) { @@ -980,7 +1027,15 @@ protected Object obtainInstanceFromSupplier(Supplier supplier, String beanNam } @Override - protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) { + protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) { + super.cacheMergedBeanDefinition(mbd, beanName); + if (mbd.isPrimary()) { + this.primaryBeanNamesWithType.put(beanName, Void.class); + } + } + + @Override + protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object @Nullable [] args) { super.checkMergedBeanDefinition(mbd, beanName, args); if (mbd.isBackgroundInit()) { @@ -991,7 +1046,7 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName } } else { - // Bean intended to be initialized in main bootstrap thread + // Bean intended to be initialized in main bootstrap thread. if (this.preInstantiationThread.get() == PreInstantiation.BACKGROUND) { throw new BeanCurrentlyInCreationException(beanName, "Bean marked for mainline initialization " + "but requested in background thread - enforce early instantiation in mainline thread " + @@ -1001,8 +1056,46 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName } @Override - protected boolean isCurrentThreadAllowedToHoldSingletonLock() { - return (this.preInstantiationThread.get() != PreInstantiation.BACKGROUND); + protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { + String mainThreadPrefix = this.mainThreadPrefix; + if (mainThreadPrefix != null) { + // We only differentiate in the preInstantiateSingletons phase, using + // the volatile mainThreadPrefix field as an indicator for that phase. + + PreInstantiation preInstantiation = this.preInstantiationThread.get(); + if (preInstantiation != null) { + // A Spring-managed bootstrap thread: + // MAIN is allowed to lock (true) or even forced to lock (null), + // BACKGROUND is never allowed to lock (false). + return switch (preInstantiation) { + case MAIN -> (Boolean.TRUE.equals(this.strictLocking) ? null : true); + case BACKGROUND -> false; + }; + } + + // Not a Spring-managed bootstrap thread... + if (Boolean.FALSE.equals(this.strictLocking)) { + // Explicitly configured to use lenient locking wherever possible. + return true; + } + else if (this.strictLocking == null) { + // No explicit locking configuration -> infer appropriate locking. + if (!getThreadNamePrefix().equals(mainThreadPrefix)) { + // An unmanaged thread (assumed to be application-internal) with lenient locking, + // and not part of the same thread pool that provided the main bootstrap thread + // (excluding scenarios where we are hit by multiple external bootstrap threads). + return true; + } + } + } + + // Traditional behavior: forced to always hold a full lock. + return null; + } + + @Override + public void prepareSingletonBootstrap() { + this.mainThreadPrefix = getThreadNamePrefix(); } @Override @@ -1016,9 +1109,12 @@ public void preInstantiateSingletons() throws BeansException { List beanNames = new ArrayList<>(this.beanDefinitionNames); // Trigger initialization of all non-lazy singleton beans... - List> futures = new ArrayList<>(); this.preInstantiationThread.set(PreInstantiation.MAIN); + if (this.mainThreadPrefix == null) { + this.mainThreadPrefix = getThreadNamePrefix(); + } try { + List> futures = new ArrayList<>(); for (String beanName : beanNames) { RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); if (!mbd.isAbstract() && mbd.isSingleton()) { @@ -1028,18 +1124,19 @@ public void preInstantiateSingletons() throws BeansException { } } } + if (!futures.isEmpty()) { + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + } + catch (CompletionException ex) { + ReflectionUtils.rethrowRuntimeException(ex.getCause()); + } + } } 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) { @@ -1053,17 +1150,23 @@ public void preInstantiateSingletons() throws BeansException { } } - @Nullable - private CompletableFuture preInstantiateSingleton(String beanName, RootBeanDefinition mbd) { + private @Nullable CompletableFuture preInstantiateSingleton(String beanName, RootBeanDefinition mbd) { if (mbd.isBackgroundInit()) { Executor executor = getBootstrapExecutor(); if (executor != null) { + // Force initialization of depends-on beans in mainline thread. String[] dependsOn = mbd.getDependsOn(); if (dependsOn != null) { for (String dep : dependsOn) { getBean(dep); } } + // Force initialization of factory reference in mainline thread. + String factoryBeanName = mbd.getFactoryBeanName(); + if (factoryBeanName != null) { + getBean(factoryBeanName); + } + // Instantiate current bean in background thread. CompletableFuture future = CompletableFuture.runAsync( () -> instantiateSingletonInBackgroundThread(beanName), executor); addSingletonFactory(beanName, () -> { @@ -1082,8 +1185,15 @@ else if (logger.isInfoEnabled()) { "without bootstrap executor configured - falling back to mainline initialization"); } } + if (!mbd.isLazyInit()) { - instantiateSingleton(beanName); + 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; } @@ -1116,6 +1226,23 @@ private void instantiateSingleton(String beanName) { } } + private Object resolveBean(String beanName, ResolvableType requiredType) { + try { + // Need to provide required type for SmartFactoryBean + return getBean(beanName, requiredType.toClass()); + } + catch (BeanNotOfRequiredTypeException ex) { + // Probably a null bean... + return 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 @@ -1163,6 +1290,11 @@ public void registerBeanDefinition(String beanName, BeanDefinition beanDefinitio } } else { + if (logger.isInfoEnabled()) { + logger.info("Removing alias '" + beanName + "' for bean '" + aliasedName + + "' due to registration of bean definition for bean '" + beanName + "': [" + + beanDefinition + "]"); + } removeAlias(beanName); } } @@ -1195,7 +1327,7 @@ else if (isConfigurationFrozen()) { // Cache a primary marker for the given bean. if (beanDefinition.isPrimary()) { - this.primaryBeanNames.add(beanName); + this.primaryBeanNamesWithType.put(beanName, Void.class); } } @@ -1204,7 +1336,7 @@ private void logBeanDefinitionOverriding(String beanName, BeanDefinition beanDef boolean explicitBeanOverride = (this.allowBeanDefinitionOverriding != null); if (existingDefinition.getRole() < beanDefinition.getRole()) { - // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE + // 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 [" + @@ -1283,11 +1415,11 @@ 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); + this.primaryBeanNamesWithType.remove(beanName); // Notify all post-processors that the specified bean definition has been reset. for (MergedBeanDefinitionPostProcessor processor : getBeanPostProcessorCache().mergedDefinition) { @@ -1337,11 +1469,30 @@ protected void checkForAliasCircle(String name, String alias) { } } + @Override + protected void addSingleton(String beanName, Object singletonObject) { + super.addSingleton(beanName, singletonObject); + + Predicate> filter = (beanType -> beanType != Object.class && beanType.isInstance(singletonObject)); + this.allBeanNamesByType.keySet().removeIf(filter); + this.singletonBeanNamesByType.keySet().removeIf(filter); + + if (this.primaryBeanNamesWithType.containsKey(beanName) && singletonObject.getClass() != NullBean.class) { + Class beanType = (singletonObject instanceof FactoryBean fb ? + getTypeForFactoryBean(fb) : singletonObject.getClass()); + if (beanType != null) { + this.primaryBeanNamesWithType.put(beanName, beanType); + } + } + } + @Override public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException { super.registerSingleton(beanName, singletonObject); + updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName)); - clearByTypeCache(); + this.allBeanNamesByType.remove(Object.class); + this.singletonBeanNamesByType.remove(Object.class); } @Override @@ -1415,9 +1566,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); @@ -1441,7 +1591,7 @@ else if (candidateNames.length > 1) { Map candidates = CollectionUtils.newLinkedHashMap(candidateNames.length); for (String beanName : candidateNames) { if (containsSingleton(beanName) && args == null) { - Object beanInstance = getBean(beanName); + Object beanInstance = resolveBean(beanName, requiredType); candidates.put(beanName, (beanInstance instanceof NullBean ? null : beanInstance)); } else { @@ -1452,6 +1602,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) { @@ -1470,11 +1623,10 @@ 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); + Object bean = (args != null ? getBean(beanName, args) : resolveBean(beanName, requiredType)); if (bean instanceof NullBean) { return null; } @@ -1482,19 +1634,18 @@ 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()); if (Optional.class == descriptor.getDependencyType()) { - return createOptionalDependency(descriptor, requestingBeanName); + return createOptionalDependency(descriptor, requestingBeanName, autowiredBeanNames, null); } 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 if (descriptor.supportsLazyResolution()) { @@ -1507,14 +1658,13 @@ else if (descriptor.supportsLazyResolution()) { return doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); } - @Nullable - @SuppressWarnings("NullAway") - public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public @Nullable Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor); 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; @@ -1522,7 +1672,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) { @@ -1558,7 +1708,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str if (autowiredBeanNames != null) { autowiredBeanNames.add(dependencyName); } - Object dependencyBean = getBean(dependencyName); + Object dependencyBean = resolveBean(dependencyName, descriptor.getResolvableType()); return resolveInstance(dependencyBean, descriptor, type, dependencyName); } } @@ -1625,8 +1775,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str } } - @Nullable - private Object resolveInstance(Object candidate, DependencyDescriptor descriptor, Class type, String name) { + private @Nullable Object resolveInstance(Object candidate, DependencyDescriptor descriptor, Class type, String name) { Object result = candidate; if (result instanceof NullBean) { // Raise exception if null encountered for required injection point @@ -1639,11 +1788,9 @@ private Object resolveInstance(Object candidate, DependencyDescriptor descriptor throw new BeanNotOfRequiredTypeException(name, type, candidate.getClass()); } return result; - } - @Nullable - private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName, + private @Nullable Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { Class type = descriptor.getDependencyType(); @@ -1699,8 +1846,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(); @@ -1714,8 +1860,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(); @@ -1741,8 +1886,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(); @@ -1775,8 +1919,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( @@ -1841,7 +1984,8 @@ protected Map findAutowireCandidates( DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch(); for (String candidate : candidateNames) { if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor) && - (!multiple || getAutowireCandidateResolver().hasQualifier(descriptor))) { + (!multiple || matchesBeanName(candidate, descriptor.getDependencyName()) || + getAutowireCandidateResolver().hasQualifier(descriptor))) { addCandidateEntry(result, candidate, descriptor, requiredType); } } @@ -1873,10 +2017,10 @@ 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)); + candidates.put(candidateName, beanInstance); } else { candidates.put(candidateName, getType(candidateName)); @@ -1891,8 +2035,7 @@ 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); @@ -1922,7 +2065,12 @@ protected String determineAutowireCandidate(Map candidates, Depe if (priorityCandidate != null) { return priorityCandidate; } - // Step 4: pick directly registered dependency + // 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(); @@ -1941,8 +2089,7 @@ protected String determineAutowireCandidate(Map candidates, Depe * @return the name of the primary candidate, or {@code null} if none found * @see #isPrimary(String, Object) */ - @Nullable - protected String determinePrimaryCandidate(Map candidates, Class requiredType) { + protected @Nullable String determinePrimaryCandidate(Map candidates, Class requiredType) { String primaryBeanName = null; // First pass: identify unique primary candidate for (Map.Entry entry : candidates.entrySet()) { @@ -1953,8 +2100,9 @@ protected String determinePrimaryCandidate(Map candidates, Class boolean candidateLocal = containsBeanDefinition(candidateBeanName); boolean primaryLocal = containsBeanDefinition(primaryBeanName); if (candidateLocal == primaryLocal) { - throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), - "more than one 'primary' bean found among candidates: " + candidates.keySet()); + String message = "more than one 'primary' bean found among candidates: " + candidates.keySet(); + logger.trace(message); + throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), message); } else if (candidateLocal) { primaryBeanName = candidateBeanName; @@ -1989,12 +2137,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(); @@ -2003,13 +2153,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 { @@ -2019,6 +2168,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; } @@ -2065,8 +2221,7 @@ private boolean isFallback(String beanName) { * @param beanInstance the bean instance to check (can be {@code null}) * @return the priority assigned to that bean or {@code null} if none is set */ - @Nullable - protected Integer getPriority(Object beanInstance) { + protected @Nullable Integer getPriority(Object beanInstance) { Comparator comparator = getDependencyComparator(); if (comparator instanceof OrderComparator orderComparator) { return orderComparator.getPriority(beanInstance); @@ -2075,12 +2230,34 @@ protected Integer getPriority(Object beanInstance) { } /** - * Determine whether the given candidate name matches the bean name or the aliases + * 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 dependency name matches the bean name or the aliases * stored in this bean definition. */ - protected boolean matchesBeanName(String beanName, @Nullable String candidateName) { - return (candidateName != null && - (candidateName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), candidateName))); + protected boolean matchesBeanName(String beanName, @Nullable String dependencyName) { + return (dependencyName != null && + (dependencyName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), dependencyName))); } /** @@ -2088,7 +2265,7 @@ protected boolean matchesBeanName(String beanName, @Nullable String candidateNam * i.e. whether the candidate points back to the original bean or to a factory method * on the original bean. */ - @Contract("null, _ -> false;_, null -> false;") + @Contract("null, _ -> false; _, null -> false;") private boolean isSelfReference(@Nullable String beanName, @Nullable String candidateName) { return (beanName != null && candidateName != null && (beanName.equals(candidateName) || (containsBeanDefinition(candidateName) && @@ -2100,8 +2277,12 @@ private boolean isSelfReference(@Nullable String beanName, @Nullable String cand * 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)) { + for (Map.Entry> candidate : this.primaryBeanNamesWithType.entrySet()) { + String candidateName = candidate.getKey(); + Class candidateType = candidate.getValue(); + if (!candidateName.equals(beanName) && (candidateType != Void.class ? + dependencyType.isAssignableFrom(candidateType) : // cached singleton class for primary bean + isTypeMatch(candidateName, dependencyType))) { // not instantiated yet or not a singleton return true; } } @@ -2156,8 +2337,8 @@ 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) { + private Optional createOptionalDependency(DependencyDescriptor descriptor, @Nullable String beanName, + @Nullable Set autowiredBeanNames, @Nullable Object @Nullable [] args) { DependencyDescriptor descriptorToUse = new NestedDependencyDescriptor(descriptor) { @Override @@ -2174,10 +2355,37 @@ public boolean usesStandardBeanLookup() { return ObjectUtils.isEmpty(args); } }; - Object result = doResolveDependency(descriptorToUse, beanName, null, null); + Object result = doResolveDependency(descriptorToUse, beanName, autowiredBeanNames, null); return (result instanceof Optional optional ? optional : Optional.ofNullable(result)); } + /** + * Public method to determine the applicable order value for a given bean. + *

    This variant implicitly obtains a corresponding bean instance from this factory. + * @param beanName the name of the bean + * @return the corresponding order value (default is {@link Ordered#LOWEST_PRECEDENCE}) + * @since 7.0 + * @see #getOrder(String, Object) + */ + public int getOrder(String beanName) { + return getOrder(beanName, getBean(beanName)); + } + + /** + * Public method to determine the applicable order value for a given bean. + * @param beanName the name of the bean + * @param beanInstance the bean instance to check + * @return the corresponding order value (default is {@link Ordered#LOWEST_PRECEDENCE}) + * @since 7.0 + * @see #getOrder(String) + */ + public int getOrder(String beanName, Object beanInstance) { + OrderComparator comparator = (getDependencyComparator() instanceof OrderComparator orderComparator ? + orderComparator : OrderComparator.INSTANCE); + return comparator.getOrder(beanInstance, + new FactoryAwareOrderSourceProvider(Collections.singletonMap(beanInstance, beanName))); + } + @Override public String toString() { @@ -2300,12 +2508,17 @@ private interface BeanObjectProvider extends ObjectProvider, Serializable */ private class DependencyObjectProvider implements BeanObjectProvider { + private static final Object NOT_CACHEABLE = new Object(); + + private static final Object NULL_VALUE = new Object(); + private final DependencyDescriptor descriptor; private final boolean optional; - @Nullable - private final String beanName; + private final @Nullable String beanName; + + private transient volatile @Nullable Object cachedValue; public DependencyObjectProvider(DependencyDescriptor descriptor, @Nullable String beanName) { this.descriptor = new NestedDependencyDescriptor(descriptor); @@ -2315,22 +2528,17 @@ public DependencyObjectProvider(DependencyDescriptor descriptor, @Nullable Strin @Override public Object getObject() throws BeansException { - if (this.optional) { - return createOptionalDependency(this.descriptor, this.beanName); - } - else { - Object result = doResolveDependency(this.descriptor, this.beanName, null, null); - if (result == null) { - throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType()); - } - return result; + Object result = getValue(); + if (result == null) { + throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType()); } + return result; } @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); + return createOptionalDependency(this.descriptor, this.beanName, null, args); } else { DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) { @@ -2348,11 +2556,10 @@ 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); + return createOptionalDependency(this.descriptor, this.beanName, null, null); } else { DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) { @@ -2388,8 +2595,7 @@ public void ifAvailable(Consumer dependencyConsumer) throws BeansExcepti } @Override - @Nullable - public Object getIfUnique() throws BeansException { + public @Nullable Object getIfUnique() throws BeansException { DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) { @Override public boolean isRequired() { @@ -2400,14 +2606,13 @@ public boolean usesStandardBeanLookup() { return true; } @Override - @Nullable - public Object resolveNotUnique(ResolvableType type, Map matchingBeans) { + public @Nullable Object resolveNotUnique(ResolvableType type, Map matchingBeans) { return null; } }; try { if (this.optional) { - return createOptionalDependency(descriptorToUse, this.beanName); + return createOptionalDependency(descriptorToUse, this.beanName, null, null); } else { return doResolveDependency(descriptorToUse, this.beanName, null, null); @@ -2432,13 +2637,42 @@ public void ifUnique(Consumer dependencyConsumer) throws BeansException } } - @Nullable - protected Object getValue() throws BeansException { + protected @Nullable Object getValue() throws BeansException { + Object value = this.cachedValue; + if (value == null) { + if (isConfigurationFrozen()) { + Set autowiredBeanNames = new LinkedHashSet<>(2); + value = resolveValue(autowiredBeanNames); + boolean cacheable = false; + if (!autowiredBeanNames.isEmpty()) { + cacheable = true; + for (String autowiredBeanName : autowiredBeanNames) { + if (!containsBean(autowiredBeanName) || !isSingleton(autowiredBeanName)) { + cacheable = false; + } + } + } + this.cachedValue = (cacheable ? (value != null ? value : NULL_VALUE) : NOT_CACHEABLE); + return value; + } + } + else if (value == NULL_VALUE) { + return null; + } + else if (value != NOT_CACHEABLE) { + return value; + } + + // Not cacheable -> fresh resolution. + return resolveValue(null); + } + + private @Nullable Object resolveValue(@Nullable Set autowiredBeanNames) { if (this.optional) { - return createOptionalDependency(this.descriptor, this.beanName); + return createOptionalDependency(this.descriptor, this.beanName, autowiredBeanNames, null); } else { - return doResolveDependency(this.descriptor, this.beanName, null, null); + return doResolveDependency(this.descriptor, this.beanName, autowiredBeanNames, null); } } @@ -2458,6 +2692,36 @@ 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) { + ResolvableType type = this.descriptor.getResolvableType(); + return Arrays.stream(beanNamesForStream(type, includeNonSingletons, true)) + .filter(name -> AutowireUtils.isAutowireCandidate(DefaultListableBeanFactory.this, name)) + .filter(name -> customFilter.test(getType(name))) + .map(name -> resolveBean(name, type)) + .filter(bean -> !(bean instanceof NullBean)); + } + + @Override + public Stream orderedStream(Predicate> customFilter, boolean includeNonSingletons) { + ResolvableType type = this.descriptor.getResolvableType(); + String[] beanNames = beanNamesForStream(type, 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 = resolveBean(beanName, type); + if (!(beanInstance instanceof NullBean)) { + matchingBeans.put(beanName, beanInstance); + } + } + } + return matchingBeans.values().stream().sorted(adaptOrderComparator(matchingBeans)); + } } @@ -2479,8 +2743,7 @@ public Jsr330Provider(DependencyDescriptor descriptor, @Nullable String beanName } @Override - @Nullable - public Object get() throws BeansException { + public @Nullable Object get() throws BeansException { return getValue(); } } @@ -2505,14 +2768,13 @@ 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); + BeanDefinition beanDefinition = getMergedBeanDefinition(beanName); List sources = new ArrayList<>(3); Object orderAttribute = beanDefinition.getAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE); if (orderAttribute != null) { @@ -2524,13 +2786,15 @@ public Object getOrderSource(Object obj) { AbstractBeanDefinition.ORDER_ATTRIBUTE + "': " + orderAttribute.getClass().getName()); } } - Method factoryMethod = beanDefinition.getResolvedFactoryMethod(); - if (factoryMethod != null) { - sources.add(factoryMethod); - } - Class targetType = beanDefinition.getTargetType(); - if (targetType != null && targetType != obj.getClass()) { - sources.add(targetType); + if (beanDefinition instanceof RootBeanDefinition rootBeanDefinition) { + Method factoryMethod = rootBeanDefinition.getResolvedFactoryMethod(); + if (factoryMethod != null) { + sources.add(factoryMethod); + } + Class targetType = rootBeanDefinition.getTargetType(); + if (targetType != null && targetType != obj.getClass()) { + sources.add(targetType); + } } return sources.toArray(); } 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 146b31b93acf..f3491647b4fe 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -24,10 +25,13 @@ 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; import org.springframework.beans.factory.BeanCurrentlyInCreationException; @@ -35,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; @@ -76,6 +79,9 @@ 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); @@ -91,23 +97,32 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements /** Set of registered singletons, containing the bean names in registration order. */ private final Set registeredSingletons = Collections.synchronizedSet(new LinkedHashSet<>(256)); - private final Lock singletonLock = new ReentrantLock(); - /** Names of beans that are currently in creation. */ private final Set singletonsCurrentlyInCreation = ConcurrentHashMap.newKeySet(16); /** Names of beans currently excluded from in creation checks. */ private final Set inCreationCheckExclusions = ConcurrentHashMap.newKeySet(16); - @Nullable - private volatile Thread singletonCreationThread; + /** 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 volatile boolean singletonsCurrentlyInDestruction = false; /** Collection of suppressed Exceptions, available for associating related causes. */ - @Nullable - private Set suppressedExceptions; + private @Nullable Set suppressedExceptions; /** Disposable bean instances: bean name to disposable instance. */ private final Map disposableBeans = new LinkedHashMap<>(); @@ -160,7 +175,7 @@ protected void addSingleton(String beanName, Object singletonObject) { /** * Add the given singleton factory for building the specified singleton * if necessary. - *

    To be called for early exposure purposes, 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 @@ -178,8 +193,7 @@ public void addSingletonCallback(String beanName, Consumer singletonCons } @Override - @Nullable - public Object getSingleton(String beanName) { + public @Nullable Object getSingleton(String beanName) { return getSingleton(beanName, true); } @@ -191,8 +205,7 @@ public Object getSingleton(String beanName) { * @param allowEarlyReference whether early references should be created or not * @return the registered singleton object, or {@code null} if none found */ - @Nullable - protected Object getSingleton(String beanName, boolean allowEarlyReference) { + protected @Nullable Object getSingleton(String beanName, boolean allowEarlyReference) { // Quick check for existing instance without full singleton lock. Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { @@ -238,41 +251,48 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { * with, if necessary * @return the registered singleton object */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Object getSingleton(String beanName, ObjectFactory singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); - boolean acquireLock = isCurrentThreadAllowedToHoldSingletonLock(); + 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) { - if (locked) { - this.singletonCreationThread = Thread.currentThread(); - } - else { - Thread threadWithLock = this.singletonCreationThread; - if (threadWithLock != null) { - // 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 (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. + this.lenientCreationLock.lock(); + try { if (logger.isInfoEnabled()) { - logger.info("Creating singleton bean '" + beanName + "' in thread \"" + - Thread.currentThread().getName() + "\" while thread \"" + threadWithLock.getName() + - "\" holds singleton lock for other beans " + this.singletonsCurrentlyInCreation); + Set lockedBeans = new HashSet<>(this.singletonsCurrentlyInCreation); + lockedBeans.removeAll(this.singletonsInLenientCreation); + logger.info("Obtaining singleton bean '" + beanName + "' in thread \"" + + currentThread.getName() + "\" while other thread holds singleton " + + "lock for other beans " + lockedBeans); } + this.singletonsInLenientCreation.add(beanName); } - else { - // Singleton lock currently held by some other registration 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; - } + 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; } } } @@ -285,16 +305,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 = (locked && this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = new LinkedHashSet<>(); } - this.singletonCreationThread = Thread.currentThread(); 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 -> @@ -313,14 +393,23 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { throw ex; } finally { - this.singletonCreationThread = null; if (recordSuppressedExceptions) { this.suppressedExceptions = null; } 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; @@ -329,22 +418,50 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { 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, any thread may acquire and hold the singleton lock, except - * background threads from {@link DefaultListableBeanFactory#setBootstrapExecutor}. + *

    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 boolean isCurrentThreadAllowedToHoldSingletonLock() { - return true; + 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}. @@ -415,7 +532,7 @@ public boolean isSingletonCurrentlyInCreation(@Nullable String beanName) { /** * Callback before singleton creation. - *

    The default implementation register the singleton as currently in creation. + *

    The default implementation registers the singleton as currently in creation. * @param beanName the name of the singleton about to be created * @see #isSingletonCurrentlyInCreation */ @@ -455,7 +572,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 @@ -465,7 +582,7 @@ public void registerDisposableBean(String beanName, DisposableBean bean) { public void registerContainedBean(String containedBeanName, String containingBeanName) { synchronized (this.containedBeanMap) { Set containedBeans = - this.containedBeanMap.computeIfAbsent(containingBeanName, k -> new LinkedHashSet<>(8)); + this.containedBeanMap.computeIfAbsent(containingBeanName, key -> new LinkedHashSet<>(8)); if (!containedBeans.add(containedBeanName)) { return; } @@ -484,7 +601,7 @@ public void registerDependentBean(String beanName, String dependentBeanName) { synchronized (this.dependentBeanMap) { Set dependentBeans = - this.dependentBeanMap.computeIfAbsent(canonicalName, k -> new LinkedHashSet<>(8)); + this.dependentBeanMap.computeIfAbsent(canonicalName, key -> new LinkedHashSet<>(8)); if (!dependentBeans.add(dependentBeanName)) { return; } @@ -492,7 +609,7 @@ public void registerDependentBean(String beanName, String dependentBeanName) { synchronized (this.dependenciesForBeanMap) { Set dependenciesForBean = - this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, k -> new LinkedHashSet<>(8)); + this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, key -> new LinkedHashSet<>(8)); dependenciesForBean.add(canonicalName); } } @@ -633,12 +750,19 @@ public void destroySingleton(String beanName) { // For an individual destruction, remove the registered instance now. // As of 6.2, this happens after the current bean's destruction step, // allowing for late bean retrieval by on-demand suppliers etc. - this.singletonLock.lock(); - try { + if (this.currentCreationThreads.get(beanName) == Thread.currentThread()) { + // Local remove after failed creation step -> without singleton lock + // since bean creation may have happened leniently without any lock. removeSingleton(beanName); } - finally { - this.singletonLock.unlock(); + else { + this.singletonLock.lock(); + try { + removeSingleton(beanName); + } + finally { + this.singletonLock.unlock(); + } } } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index 14198c4b2f1f..550d7f04ad49 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; 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 +37,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; @@ -75,7 +76,7 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable { private static final Log logger = LogFactory.getLog(DisposableBeanAdapter.class); - private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( + private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent( "org.reactivestreams.Publisher", DisposableBeanAdapter.class.getClassLoader()); @@ -89,14 +90,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 +145,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 +175,7 @@ public DisposableBeanAdapter(Object bean, List postProcessors) { this.bean = bean; @@ -253,16 +251,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); @@ -286,8 +283,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)); @@ -324,7 +320,7 @@ else if (returnValue instanceof Future future) { future.get(); logDestroyMethodCompletion(destroyMethod, true); } - else if (!reactiveStreamsPresent || !new ReactiveDestroyMethodHandler().await(destroyMethod, returnValue)) { + else if (!REACTIVE_STREAMS_PRESENT || !new ReactiveDestroyMethodHandler().await(destroyMethod, returnValue)) { if (logger.isDebugEnabled()) { logger.debug("Unknown return value type from custom destroy method '" + destroyMethod.getName() + "' on bean with name '" + this.beanName + "': " + returnValue.getClass()); @@ -408,8 +404,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; @@ -418,14 +413,29 @@ static String[] inferDestroyMethodsIfNecessary(Class target, RootBeanDefiniti String destroyMethodName = beanDefinition.resolvedDestroyMethodName; if (destroyMethodName == null) { destroyMethodName = beanDefinition.getDestroyMethodName(); - boolean autoCloseable = (AutoCloseable.class.isAssignableFrom(target)); + boolean autoCloseable = AutoCloseable.class.isAssignableFrom(target); + boolean executorService = ExecutorService.class.isAssignableFrom(target); if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) || - (destroyMethodName == null && autoCloseable)) { + (destroyMethodName == null && (autoCloseable || executorService))) { // Only perform destroy method inference in case of the bean // not explicitly implementing the DisposableBean interface destroyMethodName = null; if (!(DisposableBean.class.isAssignableFrom(target))) { - if (autoCloseable) { + if (executorService) { + destroyMethodName = SHUTDOWN_METHOD_NAME; + try { + // On JDK 19+, avoid the ExecutorService-level AutoCloseable default implementation + // which awaits task termination for 1 day, even for delayed tasks such as cron jobs. + // Custom close() implementations in ExecutorService subclasses are still accepted. + if (target.getMethod(CLOSE_METHOD_NAME).getDeclaringClass() != ExecutorService.class) { + destroyMethodName = CLOSE_METHOD_NAME; + } + } + catch (NoSuchMethodException ex) { + // Ignore - stick with shutdown() + } + } + else if (autoCloseable) { destroyMethodName = CLOSE_METHOD_NAME; } else { @@ -469,8 +479,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 bab9915aaf25..3e428b976fa2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,14 +19,16 @@ 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; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBeanNotInitializedException; +import org.springframework.beans.factory.SmartFactoryBean; 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 +52,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 +103,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); } @@ -116,44 +116,64 @@ protected Object getCachedObjectForFactoryBean(String beanName) { * @throws BeanCreationException if FactoryBean object creation failed * @see org.springframework.beans.factory.FactoryBean#getObject() */ - protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName, boolean shouldPostProcess) { + protected Object getObjectFromFactoryBean(FactoryBean factory, @Nullable Class requiredType, + String beanName, boolean shouldPostProcess) { + if (factory.isSingleton() && containsSingleton(beanName)) { - 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) - Object alreadyThere = this.factoryBeanObjectCache.get(beanName); - if (alreadyThere != null) { - object = alreadyThere; + Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock(); + boolean locked; + if (lockFlag == null) { + this.singletonLock.lock(); + locked = true; + } + else { + locked = (lockFlag && this.singletonLock.tryLock()); + } + try { + if (factory instanceof SmartFactoryBean) { + // A SmartFactoryBean may return multiple object types -> do not cache. + // Also, a SmartFactoryBean needs to be thread-safe -> no synchronization necessary. + Object object = doGetObjectFromFactoryBean(factory, requiredType, beanName); + if (shouldPostProcess) { + object = postProcessObjectFromSingletonFactoryBean(object, beanName, locked); + } + return object; } else { - if (shouldPostProcess) { - if (isSingletonCurrentlyInCreation(beanName)) { - // Temporarily return non-post-processed object, not storing it yet - return object; - } - beforeSingletonCreation(beanName); - try { - object = postProcessObjectFromFactoryBean(object, beanName); - } - catch (Throwable ex) { - throw new BeanCreationException(beanName, - "Post-processing of FactoryBean's singleton object failed", ex); - } - finally { - afterSingletonCreation(beanName); + // Defensively synchronize against non-thread-safe FactoryBean.getObject() implementations, + // potentially to be called from a background thread while the main thread currently calls + // the same getObject() method within the singleton lock. + synchronized (factory) { + Object object = this.factoryBeanObjectCache.get(beanName); + if (object == null) { + object = doGetObjectFromFactoryBean(factory, requiredType, beanName); + // Only post-process and store if not put there already during getObject() call above + // (for example, because of circular reference processing triggered by custom getBean calls) + Object alreadyThere = this.factoryBeanObjectCache.get(beanName); + if (alreadyThere != null) { + object = alreadyThere; + } + else { + if (shouldPostProcess) { + object = postProcessObjectFromSingletonFactoryBean(object, beanName, locked); + } + if (containsSingleton(beanName)) { + this.factoryBeanObjectCache.put(beanName, object); + } + } } - } - if (containsSingleton(beanName)) { - this.factoryBeanObjectCache.put(beanName, object); + return object; } } } - return object; + finally { + if (locked) { + this.singletonLock.unlock(); + } + } } else { - Object object = doGetObjectFromFactoryBean(factory, beanName); + Object object = doGetObjectFromFactoryBean(factory, requiredType, beanName); if (shouldPostProcess) { try { object = postProcessObjectFromFactoryBean(object, beanName); @@ -174,10 +194,13 @@ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanNam * @throws BeanCreationException if FactoryBean object creation failed * @see org.springframework.beans.factory.FactoryBean#getObject() */ - private Object doGetObjectFromFactoryBean(FactoryBean factory, String beanName) throws BeanCreationException { + private Object doGetObjectFromFactoryBean(FactoryBean factory, @Nullable Class requiredType, String beanName) + throws BeanCreationException { + Object object; try { - object = factory.getObject(); + object = (requiredType != null && factory instanceof SmartFactoryBean smartFactoryBean ? + smartFactoryBean.getObject(requiredType) : factory.getObject()); } catch (FactoryBeanNotInitializedException ex) { throw new BeanCurrentlyInCreationException(beanName, ex.toString()); @@ -198,6 +221,31 @@ private Object doGetObjectFromFactoryBean(FactoryBean factory, String beanNam return object; } + /** + * Post-process the given object instance produced by a singleton FactoryBean. + */ + private Object postProcessObjectFromSingletonFactoryBean(Object object, String beanName, boolean locked) { + if (locked) { + if (isSingletonCurrentlyInCreation(beanName)) { + // Temporarily return non-post-processed object, not storing it yet + return object; + } + beforeSingletonCreation(beanName); + } + try { + return postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Post-processing of FactoryBean's singleton object failed", ex); + } + finally { + if (locked) { + afterSingletonCreation(beanName); + } + } + } + /** * Post-process the given object that has been obtained from the FactoryBean. * The resulting object will get exposed for bean references. 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..a76d28d93da1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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 12c9dbb09ddf..98698f2b72bd 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,17 +29,16 @@ 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}, - * providing an implementation all non-annotation-based resolution steps at this level. + * providing an implementation for all non-annotation-based resolution steps at this level. * * @author Juergen Hoeller * @since 4.0 @@ -45,8 +46,7 @@ public class GenericTypeAwareAutowireCandidateResolver extends SimpleAutowireCandidateResolver implements BeanFactoryAware, Cloneable { - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; @Override @@ -54,8 +54,7 @@ public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } - @Nullable - protected final BeanFactory getBeanFactory() { + protected final @Nullable BeanFactory getBeanFactory() { return this.beanFactory; } @@ -73,7 +72,7 @@ public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDesc * Match the given dependency type with its generic type information against the given * candidate bean definition. */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) { ResolvableType dependencyType = descriptor.getResolvableType(); if (dependencyType.getType() instanceof Class) { @@ -147,7 +146,7 @@ protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, Dependenc } if (descriptor.fallbackMatchAllowed()) { - // Fallback matches allow unresolvable generics, e.g. plain HashMap to Map; + // 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). if (targetType.hasUnresolvableGenerics()) { @@ -161,8 +160,7 @@ else if (targetType.resolve() == Properties.class) { return dependencyType.isAssignableFrom(targetType); } - @Nullable - protected RootBeanDefinition getResolvedDecoratedDefinition(RootBeanDefinition rbd) { + protected @Nullable RootBeanDefinition getResolvedDecoratedDefinition(RootBeanDefinition rbd) { BeanDefinitionHolder decDef = rbd.getDecoratedDefinition(); if (decDef != null && this.beanFactory instanceof ConfigurableListableBeanFactory clbf) { if (clbf.containsBeanDefinition(decDef.getBeanName())) { @@ -175,8 +173,7 @@ protected RootBeanDefinition getResolvedDecoratedDefinition(RootBeanDefinition r return null; } - @Nullable - protected ResolvableType getReturnTypeForFactoryMethod(RootBeanDefinition rbd, DependencyDescriptor descriptor) { + protected @Nullable ResolvableType getReturnTypeForFactoryMethod(RootBeanDefinition rbd, DependencyDescriptor descriptor) { // Should typically be set for any kind of factory method, since the BeanFactory // pre-resolves them before reaching out to the AutowireCandidateResolver... ResolvableType returnType = rbd.factoryMethodReturnType; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ImplicitlyAppearedSingletonException.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ImplicitlyAppearedSingletonException.java index eabec72b62f3..b0c730f6fcd4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ImplicitlyAppearedSingletonException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ImplicitlyAppearedSingletonException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java index 22e65bc12be5..b74220fb17a8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,8 @@ import java.lang.reflect.Method; import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.function.ThrowingBiFunction; import org.springframework.util.function.ThrowingSupplier; @@ -59,8 +60,7 @@ default T getWithException() { * another means. * @return the factory method used to create the instance, or {@code null} */ - @Nullable - default Method getFactoryMethod() { + default @Nullable Method getFactoryMethod() { return null; } @@ -83,8 +83,7 @@ public V get(RegisteredBean registeredBean) throws Exception { return after.applyWithException(registeredBean, InstanceSupplier.this.get(registeredBean)); } @Override - @Nullable - public Method getFactoryMethod() { + public @Nullable Method getFactoryMethod() { return InstanceSupplier.this.getFactoryMethod(); } }; @@ -127,8 +126,7 @@ public T get(RegisteredBean registeredBean) throws Exception { return supplier.getWithException(); } @Override - @Nullable - public Method getFactoryMethod() { + public @Nullable Method getFactoryMethod() { return factoryMethod; } }; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java index 450d85aa9ec3..b946f921963b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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,10 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; -import org.springframework.lang.Nullable; /** * Interface responsible for creating instances corresponding to a root bean definition. @@ -80,7 +81,7 @@ Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory * @throws BeansException if the instantiation attempt failed */ Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - @Nullable Object factoryBean, Method factoryMethod, Object... args) + @Nullable Object factoryBean, Method factoryMethod, @Nullable Object... args) throws BeansException; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java index 9cbcbebe94e9..b8fd879c9725 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,11 +42,9 @@ */ public class LookupOverride extends MethodOverride { - @Nullable - private final String beanName; + private final @Nullable String beanName; - @Nullable - private Method method; + private @Nullable Method method; /** @@ -75,8 +74,7 @@ public LookupOverride(Method method, @Nullable String beanName) { /** * Return the name of the bean that should be returned by this {@code LookupOverride}. */ - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedArray.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedArray.java index 89e346b2d9b5..f89050bd654c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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..0aa83be9e266 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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..548627e5c0e8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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..93da182d0135 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,10 @@ import java.util.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..7fcdd3cba690 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.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/MergedBeanDefinitionPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MergedBeanDefinitionPostProcessor.java index 7306171ca10d..beeb56461d25 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MergedBeanDefinitionPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MergedBeanDefinitionPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 6b0e4e90a92c..3fff40069db6 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-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ * 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) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java index 49ca03408e5e..5a4f77da1280 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -42,8 +43,7 @@ public abstract class MethodOverride implements BeanMetadataElement { private boolean overloaded = true; - @Nullable - private Object source; + private @Nullable Object source; /** @@ -90,8 +90,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java index d9d9e6c12177..0a17ced99e85 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Set of method overrides, determining which, if any, methods on a @@ -87,11 +87,10 @@ public boolean isEmpty() { /** * Return the override for the given method, if any. - * @param method method to check for overrides for + * @param method the method to check for overrides for * @return the method override, or {@code null} if none */ - @Nullable - public MethodOverride getOverride(Method method) { + public @Nullable MethodOverride getOverride(Method method) { MethodOverride match = null; for (MethodOverride candidate : this.overrides) { if (candidate.matches(method)) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java index e4e5df879e5b..702fca2cf928 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ public interface MethodReplacer { * @param obj the instance we're reimplementing the method for * @param method the method to reimplement * @param args arguments to the method - * @return return value for the method + * @return the return value for the method */ Object reimplement(Object obj, Method method, Object[] args) throws Throwable; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/NullBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/NullBean.java index 7905acc09554..bd12f1935e0e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..b7a5c326dd54 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.beans.factory.support; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.util.Enumeration; import java.util.HashMap; @@ -25,6 +24,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 +36,6 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.DefaultPropertiesPersister; import org.springframework.util.PropertiesPersister; import org.springframework.util.StringUtils; @@ -74,10 +74,10 @@ * @author Rob Harrop * @since 26.11.2003 * @see DefaultListableBeanFactory - * @deprecated as of 5.3, in favor of Spring's common bean definition formats - * and/or custom reader implementations + * @deprecated in favor of Spring's common bean definition formats and/or + * custom BeanDefinitionReader implementations */ -@Deprecated +@Deprecated(since = "5.3") public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader { /** @@ -128,7 +128,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 +145,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 +164,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 +179,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 +217,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 +241,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 @@ -257,14 +255,14 @@ public int loadBeanDefinitions(EncodedResource encodedResource, @Nullable String Properties props = new Properties(); try { - try (InputStream is = encodedResource.getResource().getInputStream()) { + encodedResource.getResource().consumeContent(is -> { if (encodedResource.getEncoding() != null) { getPropertiesPersister().load(props, new InputStreamReader(is, encodedResource.getEncoding())); } else { getPropertiesPersister().load(props, is); } - } + }); int count = registerBeanDefinitions(props, prefix, encodedResource.getResource().getDescription()); if (logger.isDebugEnabled()) { @@ -294,7 +292,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 +329,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 +344,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) @@ -405,10 +403,10 @@ public int registerBeanDefinitions(Map map, @Nullable String prefix, Strin /** * Get all property values, given a prefix (which will be stripped) * and add the bean they define to the factory with the given name. - * @param beanName name of the bean to define + * @param beanName the name of the bean to define * @param map a Map containing string pairs - * @param prefix prefix of each entry, which will be stripped - * @param resourceDescription description of the resource that the + * @param prefix the prefix of each entry, which will be stripped + * @param resourceDescription the description of the resource that the * Map came from (for logging purposes) * @throws BeansException if the bean definition could not be parsed or registered */ diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java index da3b8e6ec395..20ba519a3902 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-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,8 @@ import java.util.function.BiFunction; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanDefinition; @@ -31,7 +33,6 @@ import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.core.ResolvableType; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -57,8 +58,7 @@ public final class RegisteredBean { private final Supplier mergedBeanDefinition; - @Nullable - private final RegisteredBean parent; + private final @Nullable RegisteredBean parent; private RegisteredBean(ConfigurableListableBeanFactory beanFactory, Supplier beanName, @@ -202,8 +202,7 @@ public boolean isInnerBean() { * Return the parent of this instance or {@code null} if not an inner-bean. * @return the parent */ - @Nullable - public RegisteredBean getParent() { + public @Nullable RegisteredBean getParent() { return this.parent; } @@ -245,8 +244,7 @@ public InstantiationDescriptor resolveInstantiationDescriptor() { * @return the resolved object, or {@code null} if none found * @since 6.0.9 */ - @Nullable - public Object resolveAutowiredArgument( + public @Nullable Object resolveAutowiredArgument( DependencyDescriptor descriptor, TypeConverter typeConverter, Set autowiredBeanNames) { return new ConstructorResolver((AbstractAutowireCapableBeanFactory) getBeanFactory()) @@ -266,11 +264,11 @@ 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 (e.g. for an inherited factory method). + * 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 - * @since 6.1.7 */ public record InstantiationDescriptor(Executable executable, Class targetClass) { @@ -287,13 +285,11 @@ private static class InnerBeanResolver { private final RegisteredBean parent; - @Nullable - private final String innerBeanName; + private final @Nullable String innerBeanName; private final BeanDefinition innerBeanDefinition; - @Nullable - private volatile String resolvedBeanName; + private volatile @Nullable String resolvedBeanName; InnerBeanResolver(RegisteredBean parent, @Nullable String innerBeanName, BeanDefinition innerBeanDefinition) { Assert.isInstanceOf(AbstractAutowireCapableBeanFactory.class, parent.getBeanFactory()); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java index 4fe5ad846236..e56369ac988f 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-2024 the original author or authors. + * Copyright 2002-present the original author 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.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -53,6 +55,20 @@ public ReplaceOverride(String methodName, String methodReplacerBeanName) { this.methodReplacerBeanName = methodReplacerBeanName; } + /** + * Construct a new ReplaceOverride. + * @param methodName the name of the method to override + * @param methodReplacerBeanName the bean name of the {@link MethodReplacer} + * @param typeIdentifiers a list of type identifiers for parameter types + * @since 6.2.9 + */ + public ReplaceOverride(String methodName, String methodReplacerBeanName, List typeIdentifiers) { + super(methodName); + Assert.notNull(methodReplacerBeanName, "Method replacer bean name must not be null"); + this.methodReplacerBeanName = methodReplacerBeanName; + this.typeIdentifiers.addAll(typeIdentifiers); + } + /** * Return the name of the bean implementing MethodReplacer. @@ -70,6 +86,15 @@ public void addTypeIdentifier(String identifier) { this.typeIdentifiers.add(identifier); } + /** + * Return the list of registered type identifiers (fragments of a class string). + * @since 6.2.9 + * @see #addTypeIdentifier + */ + public List getTypeIdentifiers() { + return Collections.unmodifiableList(this.typeIdentifiers); + } + @Override public boolean matches(Method method) { 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 dca421b9a377..49d6a9be4cfd 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,8 +263,7 @@ public RootBeanDefinition(RootBeanDefinition original) { @Override - @Nullable - public String getParentName() { + public @Nullable String getParentName() { return null; } @@ -299,8 +284,7 @@ public void setDecoratedDefinition(@Nullable BeanDefinitionHolder decoratedDefin /** * Return the target definition that is being decorated by this bean definition, if any. */ - @Nullable - public BeanDefinitionHolder getDecoratedDefinition() { + public @Nullable BeanDefinitionHolder getDecoratedDefinition() { return this.decoratedDefinition; } @@ -320,8 +304,7 @@ public void setQualifiedElement(@Nullable AnnotatedElement qualifiedElement) { * Otherwise, the factory method and target class will be checked. * @since 4.3.3 */ - @Nullable - public AnnotatedElement getQualifiedElement() { + public @Nullable AnnotatedElement getQualifiedElement() { return this.qualifiedElement; } @@ -346,8 +329,7 @@ public void setTargetType(@Nullable Class targetType) { * (either specified in advance or resolved on first instantiation). * @since 3.2.2 */ - @Nullable - public Class getTargetType() { + public @Nullable Class getTargetType() { if (this.resolvedTargetType != null) { return this.resolvedTargetType; } @@ -375,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); } @@ -393,8 +375,7 @@ public ResolvableType getResolvableType() { * (in which case the regular no-arg default constructor will be called) * @since 5.1 */ - @Nullable - public Constructor[] getPreferredConstructors() { + public Constructor @Nullable [] getPreferredConstructors() { Object attribute = getAttribute(PREFERRED_CONSTRUCTORS_ATTRIBUTE); if (attribute == null) { return null; @@ -451,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; } /** @@ -513,8 +488,8 @@ public Set getExternallyManagedConfigMembers() { /** * Register an externally managed configuration initialization method — - * for example, a method annotated with JSR-250's {@code javax.annotation.PostConstruct} - * or Jakarta's {@link jakarta.annotation.PostConstruct} annotation. + * for example, a method annotated with Jakarta's + * {@link jakarta.annotation.PostConstruct} annotation. *

    The supplied {@code initMethod} may be a * {@linkplain Method#getName() simple method name} or a * {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ScopeNotActiveException.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ScopeNotActiveException.java index bb7cddaf2a21..f07e9429fcd9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ /** * 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 1c7e3cb808c9..0d73299e38ab 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-2024 the original author or authors. + * Copyright 2002-present the original author 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,53 +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; - } - - @Override - @Nullable - public String getSuggestedName(DependencyDescriptor descriptor) { - return null; - } - + /** + * 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/SimpleBeanDefinitionRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleBeanDefinitionRegistry.java index e74ebc57e93a..e13c12e1fbb4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleBeanDefinitionRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleBeanDefinitionRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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 d1d98d35e5ff..c3a8d21088b9 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-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,17 @@ package org.springframework.beans.factory.support; import java.lang.reflect.Constructor; +import java.lang.reflect.InaccessibleObjectException; 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 +52,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(); + } + } } @@ -129,53 +147,42 @@ protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - @Nullable Object factoryBean, 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().isInstance(factoryBean)) { + 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 | InaccessibleObjectException 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 e93b7da2e359..7a1570c660d3 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-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.lang.annotation.Annotation; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -26,6 +27,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 +40,9 @@ 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.ParameterizedTypeReference; 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; @@ -63,6 +65,7 @@ * @author Rod Johnson * @author Juergen Hoeller * @author Sam Brannen + * @author Yanming Zhou * @since 06.01.2003 * @see DefaultListableBeanFactory */ @@ -113,49 +116,54 @@ public void addBean(String name, Object bean) { @Override public Object getBean(String name) throws BeansException { - String beanName = BeanFactoryUtils.transformedBeanName(name); - Object bean = this.beans.get(beanName); + return getBean(name, (Class) null); + } - if (bean == null) { - throw new NoSuchBeanDefinitionException(beanName, - "Defined beans are [" + StringUtils.collectionToCommaDelimitedString(this.beans.keySet()) + "]"); - } + @SuppressWarnings("unchecked") + @Override + public T getBean(String name, @Nullable Class requiredType) throws BeansException { + String beanName = BeanFactoryUtils.transformedBeanName(name); + Object bean = obtainBean(beanName); - // Don't let calling code try to dereference the - // bean factory if the bean isn't a factory - if (BeanFactoryUtils.isFactoryDereference(name) && !(bean instanceof FactoryBean)) { - throw new BeanIsNotAFactoryException(beanName, bean.getClass()); + if (BeanFactoryUtils.isFactoryDereference(name)) { + if (!(bean instanceof FactoryBean)) { + throw new BeanIsNotAFactoryException(beanName, bean.getClass()); + } } - - if (bean instanceof FactoryBean factoryBean && !BeanFactoryUtils.isFactoryDereference(name)) { + else if (bean instanceof FactoryBean factoryBean) { try { - Object exposedObject = factoryBean.getObject(); + Object exposedObject = + (factoryBean instanceof SmartFactoryBean smartFactoryBean && requiredType != null ? + smartFactoryBean.getObject(requiredType) : factoryBean.getObject()); if (exposedObject == null) { throw new BeanCreationException(beanName, "FactoryBean exposed null object"); } - return exposedObject; + bean = exposedObject; } catch (Exception ex) { throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex); } } - else { - return bean; + + if (requiredType != null && !requiredType.isInstance(bean)) { + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } + return (T) bean; } @Override @SuppressWarnings("unchecked") - public T getBean(String name, @Nullable Class requiredType) throws BeansException { + public T getBean(String name, ParameterizedTypeReference typeReference) throws BeansException { Object bean = getBean(name); - if (requiredType != null && !requiredType.isInstance(bean)) { + Type requiredType = typeReference.getType(); + if (!ResolvableType.forType(requiredType).isInstance(bean)) { throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } return (T) bean; } @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"); @@ -163,6 +171,15 @@ public Object getBean(String name, Object... args) throws BeansException { return getBean(name); } + private Object obtainBean(String beanName) { + Object bean = this.beans.get(beanName); + if (bean == null) { + throw new NoSuchBeanDefinitionException(beanName, + "Defined beans are [" + StringUtils.collectionToCommaDelimitedString(this.beans.keySet()) + "]"); + } + return bean; + } + @Override public T getBean(Class requiredType) throws BeansException { String[] beanNames = getBeanNamesForType(requiredType); @@ -178,7 +195,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"); @@ -196,6 +213,11 @@ public ObjectProvider getBeanProvider(ResolvableType requiredType) { return getBeanProvider(requiredType, true); } + @Override + public ObjectProvider getBeanProvider(ParameterizedTypeReference requiredType) { + return getBeanProvider(ResolvableType.forType(requiredType), true); + } + @Override public boolean containsBean(String name) { return this.beans.containsKey(name); @@ -203,9 +225,9 @@ public boolean containsBean(String name) { @Override public boolean isSingleton(String name) throws NoSuchBeanDefinitionException { - Object bean = getBean(name); - // In case of FactoryBean, return singleton status of created object. - if (bean instanceof FactoryBean factoryBean) { + String beanName = BeanFactoryUtils.transformedBeanName(name); + Object bean = obtainBean(beanName); + if (bean instanceof FactoryBean factoryBean && !BeanFactoryUtils.isFactoryDereference(name)) { return factoryBean.isSingleton(); } return true; @@ -213,43 +235,52 @@ public boolean isSingleton(String name) throws NoSuchBeanDefinitionException { @Override public boolean isPrototype(String name) throws NoSuchBeanDefinitionException { - Object bean = getBean(name); - // In case of FactoryBean, return prototype status of created object. - return ((bean instanceof SmartFactoryBean smartFactoryBean && smartFactoryBean.isPrototype()) || - (bean instanceof FactoryBean factoryBean && !factoryBean.isSingleton())); + String beanName = BeanFactoryUtils.transformedBeanName(name); + Object bean = obtainBean(beanName); + return (!BeanFactoryUtils.isFactoryDereference(name) && + ((bean instanceof SmartFactoryBean smartFactoryBean && smartFactoryBean.isPrototype()) || + (bean instanceof FactoryBean factoryBean && !factoryBean.isSingleton()))); } @Override public boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException { - Class type = getType(name); - return (type != null && typeToMatch.isAssignableFrom(type)); + String beanName = BeanFactoryUtils.transformedBeanName(name); + Object bean = obtainBean(beanName); + if (bean instanceof FactoryBean factoryBean && !BeanFactoryUtils.isFactoryDereference(name)) { + Class classToMatch = typeToMatch.resolve(); + return (classToMatch != null && isTypeMatch(factoryBean, classToMatch)); + } + return typeToMatch.isInstance(bean); } @Override - public boolean isTypeMatch(String name, @Nullable Class typeToMatch) throws NoSuchBeanDefinitionException { - Class type = getType(name); - return (typeToMatch == null || (type != null && typeToMatch.isAssignableFrom(type))); + public boolean isTypeMatch(String name, Class typeToMatch) throws NoSuchBeanDefinitionException { + String beanName = BeanFactoryUtils.transformedBeanName(name); + Object bean = obtainBean(beanName); + if (bean instanceof FactoryBean factoryBean && !BeanFactoryUtils.isFactoryDereference(name)) { + return isTypeMatch(factoryBean, typeToMatch); + } + return typeToMatch.isInstance(bean); + } + + private boolean isTypeMatch(FactoryBean factoryBean, Class typeToMatch) throws NoSuchBeanDefinitionException { + if (factoryBean instanceof SmartFactoryBean smartFactoryBean) { + return smartFactoryBean.supportsType(typeToMatch); + } + Class objectType = factoryBean.getObjectType(); + return (objectType != null && typeToMatch.isAssignableFrom(objectType)); } @Override - @Nullable - public Class getType(String name) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name) throws NoSuchBeanDefinitionException { return getType(name, true); } @Override - @Nullable - public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { String beanName = BeanFactoryUtils.transformedBeanName(name); - - Object bean = this.beans.get(beanName); - if (bean == null) { - throw new NoSuchBeanDefinitionException(beanName, - "Defined beans are [" + StringUtils.collectionToCommaDelimitedString(this.beans.keySet()) + "]"); - } - + Object bean = obtainBean(beanName); if (bean instanceof FactoryBean factoryBean && !BeanFactoryUtils.isFactoryDereference(name)) { - // If it's a FactoryBean, we want to look at what it creates, not the factory class. return factoryBean.getObjectType(); } return bean.getClass(); @@ -293,7 +324,7 @@ public ObjectProvider getBeanProvider(ResolvableType requiredType, boolea public T getObject() throws BeansException { String[] beanNames = getBeanNamesForType(requiredType); if (beanNames.length == 1) { - return (T) getBean(beanNames[0], requiredType); + return (T) getBean(beanNames[0], requiredType.toClass()); } else if (beanNames.length > 1) { throw new NoUniqueBeanDefinitionException(requiredType, beanNames); @@ -303,7 +334,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); @@ -316,11 +347,10 @@ 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]); + return (T) getBean(beanNames[0], requiredType.toClass()); } else if (beanNames.length > 1) { throw new NoUniqueBeanDefinitionException(requiredType, beanNames); @@ -330,11 +360,10 @@ 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]); + return (T) getBean(beanNames[0], requiredType.toClass()); } else { return null; @@ -342,11 +371,8 @@ public T getIfUnique() throws BeansException { } @Override public Stream stream() { - return Arrays.stream(getBeanNamesForType(requiredType)).map(name -> (T) getBean(name)); - } - @Override - public Stream orderedStream() { - return stream().sorted(OrderComparator.INSTANCE); + return Arrays.stream(getBeanNamesForType(requiredType)) + .map(name -> (T) getBean(name, requiredType.toClass())); } }; } @@ -360,17 +386,16 @@ public String[] getBeanNamesForType(@Nullable ResolvableType type) { public String[] getBeanNamesForType(@Nullable ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) { - Class resolved = (type != null ? type.resolve() : null); - boolean isFactoryType = resolved != null && FactoryBean.class.isAssignableFrom(resolved); + Class clazz = (type != null ? type.resolve() : null); + boolean isFactoryType = (clazz != null && FactoryBean.class.isAssignableFrom(clazz)); List matches = new ArrayList<>(); for (Map.Entry entry : this.beans.entrySet()) { String beanName = entry.getKey(); Object beanInstance = entry.getValue(); if (beanInstance instanceof FactoryBean factoryBean && !isFactoryType) { - Class objectType = factoryBean.getObjectType(); if ((includeNonSingletons || factoryBean.isSingleton()) && - objectType != null && (type == null || type.isAssignableFrom(objectType))) { + (type == null || (clazz != null && isTypeMatch(factoryBean, clazz)))) { matches.add(beanName); } } @@ -409,19 +434,14 @@ public Map getBeansOfType(@Nullable Class type, boolean includ for (Map.Entry entry : this.beans.entrySet()) { String beanName = entry.getKey(); Object beanInstance = entry.getValue(); - // Is bean a FactoryBean? if (beanInstance instanceof FactoryBean factoryBean && !isFactoryType) { - // Match object created by FactoryBean. - Class objectType = factoryBean.getObjectType(); if ((includeNonSingletons || factoryBean.isSingleton()) && - objectType != null && (type == null || type.isAssignableFrom(objectType))) { + (type == null || isTypeMatch(factoryBean, type))) { matches.put(beanName, getBean(beanName, type)); } } else { if (type == null || type.isInstance(beanInstance)) { - // If type to match is FactoryBean, return FactoryBean itself. - // Else, return bean instance. if (isFactoryType) { beanName = FACTORY_BEAN_PREFIX + beanName; } @@ -457,16 +477,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..10f769cad3b6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.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..68b7b008efce 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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..a6da8ffb3a6e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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.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/ClassNameBeanWiringInfoResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolver.java index 66e6f33c87a4..1ea1ed49f835 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..80cd2ab8f0d2 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-present the original author 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/AbstractSimpleBeanDefinitionParser.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSimpleBeanDefinitionParser.java index 015801b629c7..a3ff1ed65794 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSimpleBeanDefinitionParser.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSimpleBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..bbe84dc0a89e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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,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/BeanDefinitionDecorator.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDecorator.java index b50d70fd0b69..40df61dd2691 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDecorator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDocumentReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDocumentReader.java index 8bd14ff74ac2..791735d025aa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDocumentReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDocumentReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..c17853bf2737 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-present the original author 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,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 20fb6f3c9753..2b0305381a5b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; @@ -58,7 +59,6 @@ import org.springframework.beans.factory.support.ManagedSet; import org.springframework.beans.factory.support.MethodOverrides; import org.springframework.beans.factory.support.ReplaceOverride; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -262,8 +262,7 @@ public final XmlReaderContext getReaderContext() { * Invoke the {@link org.springframework.beans.factory.parsing.SourceExtractor} * to pull the source metadata from the supplied {@link Element}. */ - @Nullable - protected Object extractSource(Element ele) { + protected @Nullable Object extractSource(Element ele) { return this.readerContext.extractSource(ele); } @@ -388,8 +387,7 @@ public BeanDefinitionDefaults getBeanDefinitionDefaults() { * Return any patterns provided in the 'default-autowire-candidates' * attribute of the top-level {@code } element. */ - @Nullable - public String[] getAutowireCandidatePatterns() { + public String @Nullable [] getAutowireCandidatePatterns() { String candidatePattern = this.defaults.getAutowireCandidates(); return (candidatePattern != null ? StringUtils.commaDelimitedListToStringArray(candidatePattern) : null); } @@ -400,8 +398,7 @@ public String[] getAutowireCandidatePatterns() { * if there were errors during parse. Errors are reported to the * {@link org.springframework.beans.factory.parsing.ProblemReporter}. */ - @Nullable - public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { + public @Nullable BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { return parseBeanDefinitionElement(ele, null); } @@ -410,9 +407,7 @@ public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { * if there were errors during parse. Errors are reported to the * {@link org.springframework.beans.factory.parsing.ProblemReporter}. */ - @Nullable - @SuppressWarnings("NullAway") - public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { + public @Nullable BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { String id = ele.getAttribute(ID_ATTRIBUTE); String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); @@ -461,7 +456,8 @@ public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable Be } } catch (Exception ex) { - error(ex.getMessage(), ele); + String message = ex.getMessage(); + error(message == null ? "" : message, ele); return null; } } @@ -497,8 +493,7 @@ protected void checkNameUniqueness(String beanName, List aliases, Elemen * Parse the bean definition itself, without regard to name or aliases. May return * {@code null} if problems occurred during the parsing of the bean definition. */ - @Nullable - public AbstractBeanDefinition parseBeanDefinitionElement( + public @Nullable AbstractBeanDefinition parseBeanDefinitionElement( Element ele, String beanName, @Nullable BeanDefinition containingBean) { this.parseState.push(new BeanEntry(beanName)); @@ -890,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; } } @@ -906,8 +901,7 @@ public void parseQualifierElement(Element ele, AbstractBeanDefinition bd) { * Get the value of a property element. May be a list etc. * Also used for constructor arguments, "propertyName" being null in this case. */ - @Nullable - public Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) { + public @Nullable Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) { String elementName = (propertyName != null ? " element for property '" + propertyName + "'" : " element"); @@ -967,8 +961,7 @@ else if (subElement != null) { * @param ele subelement of property element; we don't know which yet * @param bd the current bean definition (if any) */ - @Nullable - public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) { + public @Nullable Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) { return parsePropertySubElement(ele, bd, null); } @@ -980,8 +973,7 @@ public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) * @param defaultValueType the default type (class name) for any * {@code } tag that might be created */ - @Nullable - public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) { + public @Nullable Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) { if (!isDefaultNamespace(ele)) { return parseNestedCustomElement(ele, bd); } @@ -1050,8 +1042,7 @@ else if (nodeNameEquals(ele, PROPS_ELEMENT)) { /** * Return a typed String value Object for the given 'idref' element. */ - @Nullable - public Object parseIdRefElement(Element ele) { + public @Nullable Object parseIdRefElement(Element ele) { // A generic reference to any name of any bean. String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE); if (!StringUtils.hasLength(refName)) { @@ -1304,8 +1295,7 @@ protected final Object buildTypedStringValueForMap(String value, String defaultT /** * Parse a key sub-element of a map element. */ - @Nullable - protected Object parseKeyElement(Element keyEle, @Nullable BeanDefinition bd, String defaultKeyTypeName) { + protected @Nullable Object parseKeyElement(Element keyEle, @Nullable BeanDefinition bd, String defaultKeyTypeName) { NodeList nl = keyEle.getChildNodes(); Element subElement = null; for (int i = 0; i < nl.getLength(); i++) { @@ -1366,8 +1356,7 @@ public boolean parseMergeAttribute(Element collectionElement) { * @param ele the element to parse * @return the resulting bean definition */ - @Nullable - public BeanDefinition parseCustomElement(Element ele) { + public @Nullable BeanDefinition parseCustomElement(Element ele) { return parseCustomElement(ele, null); } @@ -1377,8 +1366,7 @@ public BeanDefinition parseCustomElement(Element ele) { * @param containingBd the containing bean definition (if any) * @return the resulting bean definition */ - @Nullable - public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + public @Nullable BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { String namespaceUri = getNamespaceURI(ele); if (namespaceUri == null) { return null; @@ -1465,8 +1453,7 @@ else if (namespaceUri.startsWith("http://www.springframework.org/schema/")) { return originalDef; } - @Nullable - private BeanDefinitionHolder parseNestedCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + private @Nullable BeanDefinitionHolder parseNestedCustomElement(Element ele, @Nullable BeanDefinition containingBd) { BeanDefinition innerDefinition = parseCustomElement(ele, containingBd); if (innerDefinition == null) { error("Incorrect usage of element '" + ele.getNodeName() + "' in a nested manner. " + @@ -1490,8 +1477,7 @@ private BeanDefinitionHolder parseNestedCustomElement(Element ele, @Nullable Bea * different namespace identification mechanism. * @param node the node */ - @Nullable - public String getNamespaceURI(Node node) { + public @Nullable String getNamespaceURI(Node node) { return node.getNamespaceURI(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeansDtdResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeansDtdResolver.java index 16496d31b9b4..790c76709a81 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,12 @@ import 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 0e556b94bc8f..5a7758a5ec41 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-2024 the original author or authors. + * Copyright 2002-present the original author 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); } @@ -213,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..26a74d9ab71f 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-present the original author 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..6fa5c4703e89 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,11 @@ import 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..5c3882da87ae 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,11 @@ import 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..435db4b82be2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package org.springframework.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/DocumentLoader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentLoader.java index 816ac638c7d2..dad06c833c64 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentLoader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..3258bb3f2869 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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,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..5a93e1a204e2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author 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.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..cbce42225cf2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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,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..e331e5db9141 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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.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..0aa488bb7dd1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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 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..e291053d8f4b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 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..b749a4243001 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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.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..213e5131e16e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-present the original author 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.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/UtilNamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/UtilNamespaceHandler.java index 632ddd0e6086..c57c66a4eb68 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/UtilNamespaceHandler.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/UtilNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..631bb958d9de 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,11 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; 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 +48,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 +125,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); @@ -213,7 +212,7 @@ public boolean isNamespaceAware() { /** * Specify which {@link org.springframework.beans.factory.parsing.ProblemReporter} to use. *

    The default implementation is {@link org.springframework.beans.factory.parsing.FailFastProblemReporter} - * which exhibits fail fast behaviour. External tools can provide an alternative implementation + * which exhibits fail fast behavior. External tools can provide an alternative implementation * that collates errors and warnings for display in the tool UI. */ public void setProblemReporter(@Nullable ProblemReporter problemReporter) { @@ -339,12 +338,16 @@ public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefin "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); } - try (InputStream inputStream = encodedResource.getResource().getInputStream()) { - InputSource inputSource = new InputSource(inputStream); - if (encodedResource.getEncoding() != null) { - inputSource.setEncoding(encodedResource.getEncoding()); - } - return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); + try { + AtomicInteger count = new AtomicInteger(); + encodedResource.getResource().consumeContent(inputStream -> { + InputSource inputSource = new InputSource(inputStream); + if (encodedResource.getEncoding() != null) { + inputSource.setEncoding(encodedResource.getEncoding()); + } + count.addAndGet(doLoadBeanDefinitions(inputSource, encodedResource.getResource())); + }); + return count.get(); } catch (IOException ex) { throw new BeanDefinitionStoreException( diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionStoreException.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionStoreException.java index 05fa2bbf074c..c833e3fc35e7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionStoreException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionStoreException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..c0d4ac29861b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..4139318e83c5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..f5e402f2bf86 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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..09faf846a6ef 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharacterEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharacterEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,10 @@ package org.springframework.beans.propertyeditors; import java.beans.PropertyEditorSupport; +import java.util.HexFormat; + +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -30,7 +32,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 @@ -96,13 +98,12 @@ public String getAsText() { return (value != null ? value.toString() : ""); } - - private boolean isUnicodeCharacterSequence(String sequence) { + private static boolean isUnicodeCharacterSequence(String sequence) { return (sequence.startsWith(UNICODE_PREFIX) && sequence.length() == UNICODE_LENGTH); } private void setAsUnicode(String text) { - int code = Integer.parseInt(text.substring(UNICODE_PREFIX.length()), 16); + int code = HexFormat.fromHexDigits(text, UNICODE_PREFIX.length(), text.length()); setValue((char) code); } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java index ef772db749cb..3f097901c1f9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 @@ * 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..7532425316d9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,8 @@ import java.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; /** @@ -84,8 +84,8 @@ public String getAsText() { return ""; } StringJoiner sj = new StringJoiner(","); - for (Class klass : classes) { - sj.add(ClassUtils.getQualifiedName(klass)); + for (Class clazz : classes) { + sj.add(clazz.getTypeName()); } return sj.toString(); } 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..126f70718b72 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author 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.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; /** @@ -72,12 +72,7 @@ public void setAsText(String text) throws IllegalArgumentException { @Override public String getAsText() { Class clazz = (Class) getValue(); - if (clazz != null) { - return ClassUtils.getQualifiedName(clazz); - } - else { - return ""; - } + return (clazz != null ? clazz.getTypeName() : ""); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java index b6b9d318afe3..c56b50521890 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomBooleanEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomBooleanEditor.java index 5d71fca9daee..a3842e5bb162 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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..664cf7f1f666 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,8 @@ import java.util.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..425dfb4e7af2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.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..c20a4f6c2b91 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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.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..f92c3925df3b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,8 @@ import java.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/FileEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/FileEditor.java index f1b4432fb30f..02ab7db9bfde 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/FileEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/FileEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputSourceEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputSourceEditor.java index 27de84fba69a..7da02614c6e7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputSourceEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputSourceEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputStreamEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputStreamEditor.java index fca24a5418ef..e86c4a5c2e26 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author 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,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..7f9a78b14461 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-present the original author 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 70eb403d6a3d..de26e7b70671 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-2023 the original author or authors. + * Copyright 2002-present the original author 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,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. } } @@ -103,16 +103,21 @@ public void setAsText(String text) throws IllegalArgumentException { if (resource == null) { setValue(null); } - else if (nioPathCandidate && !resource.exists()) { + else if (nioPathCandidate && (!resource.isFile() || !resource.exists())) { setValue(Paths.get(text).normalize()); } else { try { - setValue(resource.getFile().toPath()); + setValue(resource.getFilePath()); } catch (IOException ex) { - throw new IllegalArgumentException( - "Could not retrieve file for " + resource + ": " + ex.getMessage()); + 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..f2cb759e8663 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.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..5d8bee649bb8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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 @@ 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..7f8dd42cd395 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author 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,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/ResourceBundleEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ResourceBundleEditor.java index 632eba0171a8..af03349e99fd 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ResourceBundleEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ResourceBundleEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java index e278c8721b65..0749c249ad6f 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-present the original author 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..ebf8efa17ae1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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,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/TimeZoneEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java index 3833c0268123..3dc5e5bda307 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java index e94e65f5a94f..da6be619e15f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,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/URLEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URLEditor.java index dba2f9cbbe54..78405b6961d1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URLEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URLEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/UUIDEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/UUIDEditor.java index 895c6cb20fb8..125c6518e0ce 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/UUIDEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/UUIDEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java index 4992e33aebd0..af619fcfe392 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-present the original author 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..9d0db8d0bec9 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-present the original author 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..1aa97f4a4873 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -29,8 +30,11 @@ * @author Jean-Pierre Pawlak * @since 26.05.2003 * @see #setToggleAscendingOnProperty + * @deprecated as severely outdated and superseded by more modern solutions, + * for example in Spring Data Commons */ -@SuppressWarnings("serial") +@Deprecated(since = "7.0.3", forRemoval = true) +@SuppressWarnings({"removal", "serial"}) public class MutableSortDefinition implements SortDefinition, Serializable { private String property = ""; 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..65a9548f2aad 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.Date; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -48,9 +49,11 @@ * @since 19.05.2003 * @param the element type * @see #getPageList() - * @see org.springframework.beans.support.MutableSortDefinition + * @deprecated as severely outdated and superseded by more modern solutions, + * for example in Spring Data Commons */ -@SuppressWarnings("serial") +@Deprecated(since = "7.0.3", forRemoval = true) +@SuppressWarnings({"removal", "serial"}) public class PagedListHolder implements Serializable { /** @@ -66,14 +69,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 +134,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 +150,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..46d513531cb3 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-present the original author 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; /** @@ -37,7 +38,11 @@ * @since 19.05.2003 * @param the type of objects that may be compared by this comparator * @see org.springframework.beans.BeanWrapper + * @deprecated as severely outdated and superseded by more modern solutions, + * for example in Spring Data Commons */ +@Deprecated(since = "7.0.3", forRemoval = true) +@SuppressWarnings("removal") public class PropertyComparator implements Comparator { protected final Log logger = LogFactory.getLog(getClass()); @@ -77,8 +82,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 +112,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..6fe5540845af 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-present the original author 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/SortDefinition.java b/spring-beans/src/main/java/org/springframework/beans/support/SortDefinition.java index e061a6bbb69e..f9de264848ae 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/SortDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/SortDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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,10 @@ * * @author Juergen Hoeller * @since 26.05.2003 + * @deprecated as severely outdated and superseded by more modern solutions, + * for example in Spring Data Commons */ +@Deprecated(since = "7.0.3", forRemoval = true) public interface SortDefinition { /** 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/BeanFactoryExtensions.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt index 1ef029900082..3bd4d67bb2ea 100644 --- a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.core.ResolvableType * This extension is not subject to type erasure and retains actual generic type arguments. * * @author Sebastien Deleuze + * @author Yanming Zhou * @since 5.0 */ inline fun BeanFactory.getBean(): T = @@ -31,15 +32,14 @@ inline fun BeanFactory.getBean(): T = /** * Extension for [BeanFactory.getBean] providing a `getBean("foo")` variant. - * Like the original Java method, this extension is subject to type erasure. + * This extension is not subject to type erasure and retains actual generic type arguments. * * @see BeanFactory.getBean(String, Class) * @author Sebastien Deleuze * @since 5.0 */ -@Suppress("EXTENSION_SHADOWED_BY_MEMBER") inline fun BeanFactory.getBean(name: String): T = - getBean(name, T::class.java) + getBean(name, (object : ParameterizedTypeReference() {})) /** * Extension for [BeanFactory.getBean] providing a `getBean(arg1, arg2)` variant. 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..f9eb6242d04a --- /dev/null +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt @@ -0,0 +1,427 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.env.Environment +import kotlin.reflect.KClass + +/** + * Contract for registering programmatically beans. + * + * Typically imported with an `@Import` annotation on `@Configuration` classes. + * ``` + * @Configuration + * @Import(MyBeanRegistrar::class) + * class MyConfiguration { + * } + * ``` + * + * In Kotlin, a bean registrar is typically created with a `BeanRegistrarDsl` to register + * beans programmatically in a concise and flexible way. + * ``` + * class MyBeanRegistrar : BeanRegistrarDsl({ + * registerBean() + * registerBean( + * name = "bar", + * prototype = true, + * lazyInit = true, + * description = "Custom description") { + * Bar(bean()) + * } + * profile("baz") { + * registerBean { Baz("Hello World!") } + * } + * }) + * ``` + * + * @author Sebastien Deleuze + * @since 7.0 + */ +@BeanRegistrarDslMarker +open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): BeanRegistrar { + + @PublishedApi + internal lateinit var registry: BeanRegistry + + /** + * The environment that can be used to get the active profile or some properties. + */ + lateinit var env: Environment + + + /** + * Apply the nested block if the given profile expression matches the + * active profiles. + * + * A profile expression may contain a simple profile name (for example + * `"production"`) or a compound expression. A compound expression allows + * for more complicated profile logic to be expressed, for example + * `"production & cloud"`. + * + * The following operators are supported in profile expressions: + * - `!` - A logical *NOT* of the profile name or compound expression + * - `&` - A logical *AND* of the profile names or compound expressions + * - `|` - A logical *OR* of the profile names or compound expressions + * + * Please note that the `&` and `|` operators may not be mixed + * without using parentheses. For example, `"a & b | c"` is not a valid + * expression: it must be expressed as `"(a & b) | c"` or `"a & (b | c)"`. + * @param expression the profile expressions to evaluate + */ + fun profile(expression: String, init: BeanRegistrarDsl.() -> Unit) { + if (env.matchesProfiles(expression)) { + init() + } + } + + /** + * Register beans using the given [BeanRegistrar]. + * @param registrar the bean registrar that will be called to register + * additional beans + */ + fun register(registrar: BeanRegistrar) { + return registry.register(registrar) + } + + /** + * Given a name, register an alias for it. + * @param name the canonical name + * @param alias the alias to be registered + * @throws IllegalStateException if the alias is already in use + * and may not be overridden + */ + fun registerAlias(name: String, alias: String) { + registry.registerAlias(name, alias); + } + + /** + * Register a bean of type [T] which will be instantiated using the + * related [resolvable constructor] + * [org.springframework.beans.BeanUtils.getResolvableConstructor] if any. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean(name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) { + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (fallback) { + it.fallback() + } + if (infrastructure) { + it.infrastructure() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + } + registry.registerBean(name, object: ParameterizedTypeReference() {}, customizer) + } + + /** + * Register a bean of type [T] which will be instantiated using the + * related [resolvable constructor] + * [org.springframework.beans.BeanUtils.getResolvableConstructor] + * if any. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + * @return the generated bean name + */ + inline fun registerBean(autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false): String { + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (fallback) { + it.fallback() + } + if (infrastructure) { + it.infrastructure() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + } + return registry.registerBean(object: ParameterizedTypeReference() {}, customizer) + } + + /** + * Register a bean of type [T] which will be instantiated using the + * provided [supplier]. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + * @param supplier the supplier to construct a bean instance + */ + inline fun registerBean(name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false, + crossinline supplier: (SupplierContextDsl.() -> T)) { + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (fallback) { + it.fallback() + } + if (infrastructure) { + it.infrastructure() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + it.supplier { + SupplierContextDsl(it, env).supplier() + } + } + registry.registerBean(name, object: ParameterizedTypeReference() {}, customizer) + } + + inline fun registerBean(autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false, + crossinline supplier: (SupplierContextDsl.() -> T)): String { + /** + * Register a bean of type [T] which will be instantiated using the + * provided [supplier]. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + * @param supplier the supplier to construct a bean instance + */ + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (infrastructure) { + it.infrastructure() + } + if (fallback) { + it.fallback() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + it.supplier { + SupplierContextDsl(it, env).supplier() + } + } + return registry.registerBean(object: ParameterizedTypeReference() {}, customizer) + } + + /** + * Determine whether a bean of the given name is already registered. + * @param name the name of the bean + * @since 7.1 + */ + fun containsBean(name: String): Boolean = registry.containsBean(name) + + /** + * Determine whether a bean of the given type is already registered. + * @param beanType the type of the bean + * @since 7.1 + */ + fun containsBean(beanType: KClass<*>): Boolean = registry.containsBean(beanType.java) + + /** + * Determine whether a bean of the given type is already registered. + * @param T the type of the bean + * @since 7.1 + */ + inline fun containsBean(): Boolean = + registry.containsBean(object: ParameterizedTypeReference() {}) + + + /** + * Context available from the bean instance supplier designed to give access + * to bean dependencies. + */ + @BeanRegistrarDslMarker + open class SupplierContextDsl(@PublishedApi internal val context: SupplierContext, val env: Environment) { + + /** + * Return the bean instance that uniquely matches the given object type, + * and potentially the name if provided, if any. + * @param T the bean type + * @param name the name of the bean + */ + inline fun bean(name: String? = null) : T = when (name) { + null -> beanProvider().getObject() + else -> context.bean(name, T::class.java) + } + + /** + * Return a provider for the specified bean, allowing for lazy on-demand + * retrieval of instances, including availability and uniqueness options. + * @param T type the bean must match; can be an interface or superclass + * @return a corresponding provider handle + */ + inline fun beanProvider() : ObjectProvider = + context.beanProvider(object : ParameterizedTypeReference() {}) + } + + 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/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt index 00ed65c25277..f50dcff316d7 100644 --- a/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-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..3c3e00c14f24 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 @@ @@ -330,7 +330,7 @@ list or are supposed to be matched generically by type. Note: A single generic argument value will just be used once, rather than - potentially matched multiple times (as of Spring 1.1). + potentially matched multiple times. constructor-arg elements are also used in conjunction with the factory-method element to construct beans using static or instance factory methods. @@ -343,14 +343,14 @@ @@ -451,8 +451,8 @@ - - - - diff --git a/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.xsd b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.xsd index d33af8ab0997..50bf142fabcf 100644 --- a/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.xsd +++ b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.xsd @@ -35,9 +35,8 @@ Specifies the java.util.Executor instance to use when invoking asynchronous methods. If not provided, an instance of org.springframework.core.task.SimpleAsyncTaskExecutor will be used by default. - Note that as of Spring 3.1.2, individual @Async methods may qualify which executor to - use, meaning that the executor specified here acts as a default for all non-qualified - @Async methods. + Note that individual @Async methods may qualify which executor to use, meaning that + the executor specified here acts as a default for all non-qualified @Async methods. ]]> @@ -144,8 +143,8 @@ required even when defining the executor as an inner bean: The executor won't be directly accessible then but will nevertheless use the specified id as the thread name prefix of the threads that it manages. - In the case of multiple task:executors, as of Spring 3.1.2 this value may be used to - qualify which executor should handle a given @Async method, e.g. @Async("executorId"). + In the case of multiple task:executors, this value may be used to + qualify which executor should handle a given @Async method, for example, @Async("executorId"). See the Javadoc for the #value attribute of Spring's @Async annotation for details. ]]> @@ -154,7 +153,7 @@ @@ -131,7 +132,7 @@ Singletons are most commonly used, and are ideal for multi-threaded service objects. Further scopes, such as "request" or "session", might - be supported by extended bean factories (e.g. in a web environment). + be supported by extended bean factories (for example, in a web environment). ]]> diff --git a/spring-context/src/test/java/example/gh24375/AnnotatedComponent.java b/spring-context/src/test/java/example/gh24375/AnnotatedComponent.java index 4eb7fdefb73d..238e98ad7f35 100644 --- a/spring-context/src/test/java/example/gh24375/AnnotatedComponent.java +++ b/spring-context/src/test/java/example/gh24375/AnnotatedComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/gh24375/EnclosingAnnotation.java b/spring-context/src/test/java/example/gh24375/EnclosingAnnotation.java index 1a925de59ae1..b49e2ce4ee8a 100644 --- a/spring-context/src/test/java/example/gh24375/EnclosingAnnotation.java +++ b/spring-context/src/test/java/example/gh24375/EnclosingAnnotation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/gh24375/NestedAnnotation.java b/spring-context/src/test/java/example/gh24375/NestedAnnotation.java index 531de72d6bfd..6ab5b0dfb6ea 100644 --- a/spring-context/src/test/java/example/gh24375/NestedAnnotation.java +++ b/spring-context/src/test/java/example/gh24375/NestedAnnotation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/indexed/IndexedJakartaManagedBeanComponent.java b/spring-context/src/test/java/example/indexed/IndexedJakartaManagedBeanComponent.java deleted file mode 100644 index ed640a7a73da..000000000000 --- a/spring-context/src/test/java/example/indexed/IndexedJakartaManagedBeanComponent.java +++ /dev/null @@ -1,24 +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 example.indexed; - -/** - * @author Sam Brannen - */ -@jakarta.annotation.ManagedBean -public class IndexedJakartaManagedBeanComponent { -} diff --git a/spring-context/src/test/java/example/indexed/IndexedJakartaNamedComponent.java b/spring-context/src/test/java/example/indexed/IndexedJakartaNamedComponent.java index a2b1ed2042e0..35f985f7c3e5 100644 --- a/spring-context/src/test/java/example/indexed/IndexedJakartaNamedComponent.java +++ b/spring-context/src/test/java/example/indexed/IndexedJakartaNamedComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/indexed/IndexedJavaxManagedBeanComponent.java b/spring-context/src/test/java/example/indexed/IndexedJavaxManagedBeanComponent.java deleted file mode 100644 index b563b4d37973..000000000000 --- a/spring-context/src/test/java/example/indexed/IndexedJavaxManagedBeanComponent.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package example.indexed; - -/** - * @author Sam Brannen - */ -@javax.annotation.ManagedBean -public class IndexedJavaxManagedBeanComponent { -} diff --git a/spring-context/src/test/java/example/indexed/IndexedJavaxNamedComponent.java b/spring-context/src/test/java/example/indexed/IndexedJavaxNamedComponent.java deleted file mode 100644 index 581be8a6f97d..000000000000 --- a/spring-context/src/test/java/example/indexed/IndexedJavaxNamedComponent.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package example.indexed; - -/** - * @author Sam Brannen - */ -@javax.inject.Named("myIndexedJavaxNamedComponent") -public class IndexedJavaxNamedComponent { -} diff --git a/spring-context/src/test/java/example/profilescan/DevComponent.java b/spring-context/src/test/java/example/profilescan/DevComponent.java index 6926102be871..09cf3437cb0e 100644 --- a/spring-context/src/test/java/example/profilescan/DevComponent.java +++ b/spring-context/src/test/java/example/profilescan/DevComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/profilescan/ProfileAnnotatedComponent.java b/spring-context/src/test/java/example/profilescan/ProfileAnnotatedComponent.java index 0751da179509..103c7308c6dc 100644 --- a/spring-context/src/test/java/example/profilescan/ProfileAnnotatedComponent.java +++ b/spring-context/src/test/java/example/profilescan/ProfileAnnotatedComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/profilescan/ProfileMetaAnnotatedComponent.java b/spring-context/src/test/java/example/profilescan/ProfileMetaAnnotatedComponent.java index cabbc1a179f8..069cc627a292 100644 --- a/spring-context/src/test/java/example/profilescan/ProfileMetaAnnotatedComponent.java +++ b/spring-context/src/test/java/example/profilescan/ProfileMetaAnnotatedComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/profilescan/SomeAbstractClass.java b/spring-context/src/test/java/example/profilescan/SomeAbstractClass.java index f15ca6857590..42e42f8c6b14 100644 --- a/spring-context/src/test/java/example/profilescan/SomeAbstractClass.java +++ b/spring-context/src/test/java/example/profilescan/SomeAbstractClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/AutowiredQualifierFooService.java b/spring-context/src/test/java/example/scannable/AutowiredQualifierFooService.java index 69f09081dc19..57a265e90745 100644 --- a/spring-context/src/test/java/example/scannable/AutowiredQualifierFooService.java +++ b/spring-context/src/test/java/example/scannable/AutowiredQualifierFooService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 example.scannable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import jakarta.annotation.PostConstruct; @@ -51,9 +52,8 @@ public String foo(int id) { } @Override - @SuppressWarnings("deprecation") public Future asyncFoo(int id) { - return new org.springframework.scheduling.annotation.AsyncResult<>(this.fooDao.findFoo(id)); + return CompletableFuture.completedFuture(this.fooDao.findFoo(id)); } @Override diff --git a/spring-context/src/test/java/example/scannable/CustomAnnotations.java b/spring-context/src/test/java/example/scannable/CustomAnnotations.java index c81559931eeb..d618a1999b05 100644 --- a/spring-context/src/test/java/example/scannable/CustomAnnotations.java +++ b/spring-context/src/test/java/example/scannable/CustomAnnotations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/CustomAspectStereotype.java b/spring-context/src/test/java/example/scannable/CustomAspectStereotype.java index 9c95e2b4fd99..32d20cd899e2 100644 --- a/spring-context/src/test/java/example/scannable/CustomAspectStereotype.java +++ b/spring-context/src/test/java/example/scannable/CustomAspectStereotype.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/CustomComponent.java b/spring-context/src/test/java/example/scannable/CustomComponent.java index dc1e916ea012..473d68d101e2 100644 --- a/spring-context/src/test/java/example/scannable/CustomComponent.java +++ b/spring-context/src/test/java/example/scannable/CustomComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/CustomStereotype.java b/spring-context/src/test/java/example/scannable/CustomStereotype.java index c0b3024ecbba..d0f1dd73db36 100644 --- a/spring-context/src/test/java/example/scannable/CustomStereotype.java +++ b/spring-context/src/test/java/example/scannable/CustomStereotype.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/DefaultNamedComponent.java b/spring-context/src/test/java/example/scannable/DefaultNamedComponent.java index 1047c9365874..a340d40565ea 100644 --- a/spring-context/src/test/java/example/scannable/DefaultNamedComponent.java +++ b/spring-context/src/test/java/example/scannable/DefaultNamedComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/FooDao.java b/spring-context/src/test/java/example/scannable/FooDao.java index 76bf94b79ef7..ba37f8dc5338 100644 --- a/spring-context/src/test/java/example/scannable/FooDao.java +++ b/spring-context/src/test/java/example/scannable/FooDao.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/FooService.java b/spring-context/src/test/java/example/scannable/FooService.java index 90cd3a4f1176..866b75a9b4b8 100644 --- a/spring-context/src/test/java/example/scannable/FooService.java +++ b/spring-context/src/test/java/example/scannable/FooService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/FooServiceImpl.java b/spring-context/src/test/java/example/scannable/FooServiceImpl.java index 11cd8390e688..441d5282cb30 100644 --- a/spring-context/src/test/java/example/scannable/FooServiceImpl.java +++ b/spring-context/src/test/java/example/scannable/FooServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.Comparator; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import jakarta.annotation.PostConstruct; @@ -32,6 +33,7 @@ import org.springframework.context.MessageSource; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePatternResolver; @@ -42,7 +44,7 @@ * @author Mark Fisher * @author Juergen Hoeller */ -@Service @Lazy @DependsOn("myNamedComponent") +@Service @Primary @Lazy @DependsOn("myNamedComponent") public abstract class FooServiceImpl implements FooService { // Just to test ASM5's bytecode parsing of INVOKESPECIAL/STATIC on interfaces @@ -91,10 +93,9 @@ public String lookupFoo(int id) { } @Override - @SuppressWarnings("deprecation") public Future asyncFoo(int id) { Assert.state(ServiceInvocationCounter.getThreadLocalCount() != null, "Thread-local counter not exposed"); - return new org.springframework.scheduling.annotation.AsyncResult<>(fooDao().findFoo(id)); + return CompletableFuture.completedFuture(fooDao().findFoo(id)); } @Override diff --git a/spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java b/spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java deleted file mode 100644 index 6140ea0dce36..000000000000 --- a/spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package example.scannable; - -/** - * @author Sam Brannen - */ -@jakarta.annotation.ManagedBean("myJakartaManagedBeanComponent") -public class JakartaManagedBeanComponent { -} diff --git a/spring-context/src/test/java/example/scannable/JakartaNamedComponent.java b/spring-context/src/test/java/example/scannable/JakartaNamedComponent.java index 64165df69c34..0ca393ad2a59 100644 --- a/spring-context/src/test/java/example/scannable/JakartaNamedComponent.java +++ b/spring-context/src/test/java/example/scannable/JakartaNamedComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/JavaxManagedBeanComponent.java b/spring-context/src/test/java/example/scannable/JavaxManagedBeanComponent.java deleted file mode 100644 index b3029035d874..000000000000 --- a/spring-context/src/test/java/example/scannable/JavaxManagedBeanComponent.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package example.scannable; - -/** - * @author Sam Brannen - */ -@javax.annotation.ManagedBean("myJavaxManagedBeanComponent") -public class JavaxManagedBeanComponent { -} diff --git a/spring-context/src/test/java/example/scannable/JavaxNamedComponent.java b/spring-context/src/test/java/example/scannable/JavaxNamedComponent.java deleted file mode 100644 index a0fe78e7429a..000000000000 --- a/spring-context/src/test/java/example/scannable/JavaxNamedComponent.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package example.scannable; - -/** - * @author Sam Brannen - */ -@javax.inject.Named("myJavaxNamedComponent") -public class JavaxNamedComponent { -} diff --git a/spring-context/src/test/java/example/scannable/MessageBean.java b/spring-context/src/test/java/example/scannable/MessageBean.java index ed6c0ed03219..0ad8bac7160e 100644 --- a/spring-context/src/test/java/example/scannable/MessageBean.java +++ b/spring-context/src/test/java/example/scannable/MessageBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/NamedComponent.java b/spring-context/src/test/java/example/scannable/NamedComponent.java index 3dede45edee9..7ae685cc0144 100644 --- a/spring-context/src/test/java/example/scannable/NamedComponent.java +++ b/spring-context/src/test/java/example/scannable/NamedComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/NamedStubDao.java b/spring-context/src/test/java/example/scannable/NamedStubDao.java index bb269ff5b359..c63aeba7391c 100644 --- a/spring-context/src/test/java/example/scannable/NamedStubDao.java +++ b/spring-context/src/test/java/example/scannable/NamedStubDao.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/OtherFooService.java b/spring-context/src/test/java/example/scannable/OtherFooService.java new file mode 100644 index 000000000000..23fe99961b94 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/OtherFooService.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example.scannable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +import org.springframework.context.annotation.Proxyable; +import org.springframework.stereotype.Service; + +/** + * @author Juergen Hoeller + */ +@Service @Proxyable(interfaces = FooService.class) +public class OtherFooService implements FooService { + + @Override + public String foo(int id) { + return "" + id; + } + + @Override + public Future asyncFoo(int id) { + return CompletableFuture.completedFuture("" + id); + } + + @Override + public boolean isInitCalled() { + return false; + } + +} diff --git a/spring-context/src/test/java/example/scannable/PackageMarker.java b/spring-context/src/test/java/example/scannable/PackageMarker.java index 5ee79482a7ec..ebf6a44bdaf9 100644 --- a/spring-context/src/test/java/example/scannable/PackageMarker.java +++ b/spring-context/src/test/java/example/scannable/PackageMarker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/ScopedProxyTestBean.java b/spring-context/src/test/java/example/scannable/ScopedProxyTestBean.java index 84829191179e..36328858949c 100644 --- a/spring-context/src/test/java/example/scannable/ScopedProxyTestBean.java +++ b/spring-context/src/test/java/example/scannable/ScopedProxyTestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 example.scannable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import org.springframework.context.annotation.Scope; @@ -33,9 +34,8 @@ public String foo(int id) { } @Override - @SuppressWarnings("deprecation") public Future asyncFoo(int id) { - return new org.springframework.scheduling.annotation.AsyncResult<>("bar"); + return CompletableFuture.completedFuture("bar"); } @Override diff --git a/spring-context/src/test/java/example/scannable/ServiceInvocationCounter.java b/spring-context/src/test/java/example/scannable/ServiceInvocationCounter.java index 17d17d62f044..427f5eee9144 100644 --- a/spring-context/src/test/java/example/scannable/ServiceInvocationCounter.java +++ b/spring-context/src/test/java/example/scannable/ServiceInvocationCounter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/StubFooDao.java b/spring-context/src/test/java/example/scannable/StubFooDao.java index 3a4bce678a69..354fad7c7c15 100644 --- a/spring-context/src/test/java/example/scannable/StubFooDao.java +++ b/spring-context/src/test/java/example/scannable/StubFooDao.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable/sub/BarComponent.java b/spring-context/src/test/java/example/scannable/sub/BarComponent.java index fbc2ef833491..e2c22e40da57 100644 --- a/spring-context/src/test/java/example/scannable/sub/BarComponent.java +++ b/spring-context/src/test/java/example/scannable/sub/BarComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java b/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java index c4b8cd30e4a4..e8e5f42a866e 100644 --- a/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java +++ b/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable_implicitbasepackage/ConfigurableComponent.java b/spring-context/src/test/java/example/scannable_implicitbasepackage/ConfigurableComponent.java index 3ca2181992fc..306b4adc43a2 100644 --- a/spring-context/src/test/java/example/scannable_implicitbasepackage/ConfigurableComponent.java +++ b/spring-context/src/test/java/example/scannable_implicitbasepackage/ConfigurableComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java b/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java index 92fe69111523..a96d57de1dc9 100644 --- a/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java +++ b/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java b/spring-context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java index beb55311b907..5174aba666ac 100644 --- a/spring-context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java +++ b/spring-context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/example/scannable_scoped/MyScope.java b/spring-context/src/test/java/example/scannable_scoped/MyScope.java index 8f58d6c65d41..31f9398282dd 100644 --- a/spring-context/src/test/java/example/scannable_scoped/MyScope.java +++ b/spring-context/src/test/java/example/scannable_scoped/MyScope.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AdviceBindingTestAspect.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AdviceBindingTestAspect.java index c6afc8775bc4..0fb0dbe3af82 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/AdviceBindingTestAspect.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AdviceBindingTestAspect.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AfterAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterAdviceBindingTests.java index db6589771595..43daa80eb49d 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/AfterAdviceBindingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterAdviceBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.java index f487bc710a4b..d7b65111ee6b 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.java index f447f7d78696..02721100a3b0 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceBindingTests.java index 5c3bf3814308..5c3069e18802 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceBindingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,25 +67,25 @@ void onSetUp() throws Exception { } @Test - void testOneIntArg() { + void oneIntArg() { testBeanProxy.setAge(5); verify(mockCollaborator).oneIntArg(5); } @Test - void testOneObjectArgBoundToTarget() { + void oneObjectArgBoundToTarget() { testBeanProxy.getAge(); verify(mockCollaborator).oneObjectArg(this.testBeanTarget); } @Test - void testOneIntAndOneObjectArgs() { + void oneIntAndOneObjectArgs() { testBeanProxy.setAge(5); verify(mockCollaborator).oneIntAndOneObject(5, this.testBeanProxy); } @Test - void testJustJoinPoint() { + void justJoinPoint() { testBeanProxy.getAge(); verify(mockCollaborator).justJoinPoint("getAge"); } diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceCircularTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceCircularTests.java index a16529485db7..ea9ceb33b24a 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceCircularTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceCircularTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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 @@ class AroundAdviceCircularTests extends AroundAdviceBindingTests { @Test - void testBothBeansAreProxies() { + void bothBeansAreProxies() { Object tb = ctx.getBean("testBean"); assertThat(AopUtils.isAopProxy(tb)).isTrue(); Object tb2 = ctx.getBean("testBean2"); diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java index c7f56212bc4d..b81c02aa4402 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.lang.reflect.Method; import org.aspectj.lang.ProceedingJoinPoint; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,7 +29,6 @@ import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; /** * @author Adrian Colyer @@ -66,7 +66,7 @@ void tearDown() { @Test - void testAdviceOrder() { + void adviceOrder() { PrecedenceTestAspect.Collaborator collaborator = new PrecedenceVerifyingCollaborator(); this.highPrecedenceAspect.setCollaborator(collaborator); this.lowPrecedenceAspect.setCollaborator(collaborator); @@ -106,8 +106,8 @@ private static class PrecedenceVerifyingCollaborator implements PrecedenceTestAs private void checkAdvice(String whatJustHappened) { //System.out.println("[" + adviceInvocationNumber + "] " + whatJustHappened + " ==> " + EXPECTED[adviceInvocationNumber]); if (adviceInvocationNumber > (EXPECTED.length - 1)) { - throw new AssertionError("Too many advice invocations, expecting " + EXPECTED.length - + " but had " + adviceInvocationNumber); + throw new AssertionError("Too many advice invocations, expecting " + EXPECTED.length + + " but had " + adviceInvocationNumber); } String expecting = EXPECTED[adviceInvocationNumber++]; if (!whatJustHappened.equals(expecting)) { diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.java index 8063009453ee..582ece847226 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.java index 84e1f8356e5d..ab9f6ce25504 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,8 +66,7 @@ void tearDown() { @Test void matchingBeanName() { - boolean condition = testBean1 instanceof Advised; - assertThat(condition).as("Expected a proxy").isTrue(); + assertThat(testBean1).as("Expected a proxy").isInstanceOf(Advised.class); // Call two methods to test for SPR-3953-like condition testBean1.setAge(20); @@ -77,8 +76,7 @@ void matchingBeanName() { @Test void nonMatchingBeanName() { - boolean condition = testBean3 instanceof Advised; - assertThat(condition).as("Didn't expect a proxy").isFalse(); + assertThat(testBean3).as("Didn't expect a proxy").isNotInstanceOf(Advised.class); testBean3.setAge(20); assertThat(counterAspect.count).isEqualTo(0); @@ -96,8 +94,7 @@ void programmaticProxyCreation() { ITestBean proxyTestBean = factory.getProxy(); - boolean condition = proxyTestBean instanceof Advised; - assertThat(condition).as("Expected a proxy").isTrue(); + assertThat(proxyTestBean).as("Expected a proxy").isInstanceOf(Advised.class); proxyTestBean.setAge(20); assertThat(myCounterAspect.count).as("Programmatically created proxy shouldn't match bean()").isEqualTo(0); } diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java index ed740ab88ce9..cf467bf3a7fd 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.lang.reflect.Method; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,7 +28,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; @@ -74,9 +74,8 @@ void setup() { // We don't need to test all combination of pointcuts due to BeanNamePointcutMatchingTests @Test - void testMatchingBeanName() { - boolean condition = this.testBean1 instanceof Advised; - assertThat(condition).as("Matching bean must be advised (proxied)").isTrue(); + void matchingBeanName() { + assertThat(this.testBean1).as("Matching bean must be advised (proxied)").isInstanceOf(Advised.class); // Call two methods to test for SPR-3953-like condition this.testBean1.setAge(20); this.testBean1.setName(""); @@ -84,49 +83,41 @@ void testMatchingBeanName() { } @Test - void testNonMatchingBeanName() { - boolean condition = this.testBean2 instanceof Advised; - assertThat(condition).as("Non-matching bean must *not* be advised (proxied)").isFalse(); + void nonMatchingBeanName() { + assertThat(this.testBean2).as("Non-matching bean must *not* be advised (proxied)").isNotInstanceOf(Advised.class); this.testBean2.setAge(20); assertThat(this.counterAspect.getCount()).as("Advice must *not* have been executed").isEqualTo(0); } @Test - void testNonMatchingNestedBeanName() { - boolean condition = this.testBeanContainingNestedBean.getDoctor() instanceof Advised; - assertThat(condition).as("Non-matching bean must *not* be advised (proxied)").isFalse(); + void nonMatchingNestedBeanName() { + assertThat(this.testBeanContainingNestedBean.getDoctor()).as("Non-matching bean must *not* be advised (proxied)").isNotInstanceOf(Advised.class); } @Test - void testMatchingFactoryBeanObject() { - boolean condition1 = this.testFactoryBean1 instanceof Advised; - assertThat(condition1).as("Matching bean must be advised (proxied)").isTrue(); + void matchingFactoryBeanObject() { + assertThat(this.testFactoryBean1).as("Matching bean must be advised (proxied)").isInstanceOf(Advised.class); assertThat(this.testFactoryBean1.get("myKey")).isEqualTo("myValue"); assertThat(this.testFactoryBean1.get("myKey")).isEqualTo("myValue"); assertThat(this.counterAspect.getCount()).as("Advice not executed: must have been").isEqualTo(2); FactoryBean fb = (FactoryBean) ctx.getBean("&testFactoryBean1"); - boolean condition = !(fb instanceof Advised); - assertThat(condition).as("FactoryBean itself must *not* be advised").isTrue(); + assertThat(fb).as("FactoryBean itself must *not* be advised").isNotInstanceOf(Advised.class); } @Test - void testMatchingFactoryBeanItself() { - boolean condition1 = !(this.testFactoryBean2 instanceof Advised); - assertThat(condition1).as("Matching bean must *not* be advised (proxied)").isTrue(); + void matchingFactoryBeanItself() { + assertThat(this.testFactoryBean2).as("Matching bean must *not* be advised (proxied)").isNotInstanceOf(Advised.class); FactoryBean fb = (FactoryBean) ctx.getBean("&testFactoryBean2"); - boolean condition = fb instanceof Advised; - assertThat(condition).as("FactoryBean itself must be advised").isTrue(); + assertThat(fb).as("FactoryBean itself must be advised").isInstanceOf(Advised.class); assertThat(Map.class.isAssignableFrom(fb.getObjectType())).isTrue(); assertThat(Map.class.isAssignableFrom(fb.getObjectType())).isTrue(); assertThat(this.counterAspect.getCount()).as("Advice not executed: must have been").isEqualTo(2); } @Test - void testPointcutAdvisorCombination() { - boolean condition = this.interceptThis instanceof Advised; - assertThat(condition).as("Matching bean must be advised (proxied)").isTrue(); - boolean condition1 = this.dontInterceptThis instanceof Advised; - assertThat(condition1).as("Non-matching bean must *not* be advised (proxied)").isFalse(); + void pointcutAdvisorCombination() { + assertThat(this.interceptThis).as("Matching bean must be advised (proxied)").isInstanceOf(Advised.class); + assertThat(this.dontInterceptThis).as("Non-matching bean must *not* be advised (proxied)").isNotInstanceOf(Advised.class); interceptThis.setAge(20); assertThat(testInterceptor.interceptionCount).isEqualTo(1); dontInterceptThis.setAge(20); diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/BeforeAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/BeforeAdviceBindingTests.java index 5cdb2f1b67a4..7db6d6e7c74f 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/BeforeAdviceBindingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/BeforeAdviceBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/Counter.java b/spring-context/src/test/java/org/springframework/aop/aspectj/Counter.java index a24a6d6a02cf..0cc151ede914 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/Counter.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/Counter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.java index 1083de70fd0d..ba389303fc5d 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.java index 4fd648f41a67..ab2e757e9a5d 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsTests.java index a063eb7fb938..a45eabd2e025 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ICounter.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ICounter.java index 5c2e5d15a028..6c41e152428a 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/ICounter.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ICounter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.java index e1a2b782b946..15ee5f73c389 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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 @@ class ImplicitJPArgumentMatchingAtAspectJTests { @Test - void testAspect() { + void aspect() { // nothing to really test; it is enough if we don't get error while creating the app context new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); } diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.java index a9493323b2a9..f751d3f95db0 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ class ImplicitJPArgumentMatchingTests { @Test @SuppressWarnings("resource") - void testAspect() { + void aspect() { // nothing to really test; it is enough if we don't get error while creating app context new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); } diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java index 257b6bf37580..1554396a17c9 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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,13 +34,13 @@ class OverloadedAdviceTests { @Test @SuppressWarnings("resource") - void testConfigParsingWithMismatchedAdviceMethod() { + void configParsingWithMismatchedAdviceMethod() { new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); } @Test @SuppressWarnings("resource") - void testExceptionOnConfigParsingWithAmbiguousAdviceMethod() { + void exceptionOnConfigParsingWithAmbiguousAdviceMethod() { assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ambiguous.xml", getClass())) .havingRootCause() diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ProceedTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ProceedTests.java index 2a65204fa800..9e33ca27dc05 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/ProceedTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ProceedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,19 +60,19 @@ void tearDown() { @Test - void testSimpleProceedWithChangedArgs() { + void simpleProceedWithChangedArgs() { this.testBean.setName("abc"); assertThat(this.testBean.getName()).as("Name changed in around advice").isEqualTo("ABC"); } @Test - void testGetArgsIsDefensive() { + void getArgsIsDefensive() { this.testBean.setAge(5); assertThat(this.testBean.getAge()).as("getArgs is defensive").isEqualTo(5); } @Test - void testProceedWithArgsInSameAspect() { + void proceedWithArgsInSameAspect() { this.testBean.setMyFloat(1.0F); assertThat(this.testBean.getMyFloat()).as("value changed in around advice").isGreaterThan(1.9F); assertThat(this.firstTestAspect.getLastBeforeFloatValue()).as("changed value visible to next advice in chain") @@ -80,7 +80,7 @@ void testProceedWithArgsInSameAspect() { } @Test - void testProceedWithArgsAcrossAspects() { + void proceedWithArgsAcrossAspects() { this.testBean.setSex("male"); assertThat(this.testBean.getSex()).as("value changed in around advice").isEqualTo("MALE"); assertThat(this.secondTestAspect.getLastBeforeStringValue()).as("changed value visible to next before advice in chain").isEqualTo("MALE"); diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/PropertyDependentAspectTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/PropertyDependentAspectTests.java index 48f88c9d6473..053628b1c13d 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/PropertyDependentAspectTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/PropertyDependentAspectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,8 +61,7 @@ void propertyDependentAtAspectJAspectWithPropertyDeclaredAfterAdvice() { private void checkXmlAspect(String appContextFile) { ApplicationContext context = new ClassPathXmlApplicationContext(appContextFile, getClass()); ICounter counter = (ICounter) context.getBean("counter"); - boolean condition = counter instanceof Advised; - assertThat(condition).as("Proxy didn't get created").isTrue(); + assertThat(counter).as("Proxy didn't get created").isInstanceOf(Advised.class); counter.increment(); JoinPointMonitorAspect callCountingAspect = (JoinPointMonitorAspect)context.getBean("monitoringAspect"); @@ -73,8 +72,7 @@ private void checkXmlAspect(String appContextFile) { private void checkAtAspectJAspect(String appContextFile) { ApplicationContext context = new ClassPathXmlApplicationContext(appContextFile, getClass()); ICounter counter = (ICounter) context.getBean("counter"); - boolean condition = counter instanceof Advised; - assertThat(condition).as("Proxy didn't get created").isTrue(); + assertThat(counter).as("Proxy didn't get created").isInstanceOf(Advised.class); counter.increment(); JoinPointMonitorAtAspectJAspect callCountingAspect = (JoinPointMonitorAtAspectJAspect)context.getBean("monitoringAspect"); diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.java index f9fc99209d61..d6ecb60cfb49 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.java index 917a9f7802f7..3d05b13d014c 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/TargetPointcutSelectionTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/TargetPointcutSelectionTests.java index c501cc3941bf..84c85e4c1f09 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/TargetPointcutSelectionTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/TargetPointcutSelectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.java index 9a07f3f5e1cc..ddc85cf20d95 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.java index ae3c314aa1bf..68cc41bc29b6 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBean.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBean.java index 63e063657dbb..d17b5aa6ba38 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBean.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBeanImpl.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBeanImpl.java index a05c91595040..446002b0065b 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBeanImpl.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBeanImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTestAspect.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTestAspect.java index b4bb26d719ad..2d60ce35327f 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTestAspect.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTestAspect.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests.java index 7e852be81cfa..d0aef382279f 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests.java index 82cd932a2bc1..c1932ea437d8 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests.java index 8a3738ba91bd..98cbb00afcb6 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests.java index 980698fd25b2..040583a965bd 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ class AspectJAutoProxyCreatorAndLazyInitTargetSourceTests { @Test - void testAdrian() { + void adrian() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java index 64f670650ca8..16ab1ad88a01 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -66,13 +67,12 @@ import org.springframework.core.DecoratingProxy; import org.springframework.core.NestedRuntimeException; import org.springframework.core.annotation.Order; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for AspectJ auto-proxying. Includes mixing with Spring AOP Advisors - * to demonstrate that existing autoproxying contract is honoured. + * to demonstrate that existing autoproxying contract is honored. * * @author Rod Johnson * @author Juergen Hoeller @@ -85,7 +85,7 @@ class AspectJAutoProxyCreatorTests { void aspectsAreApplied() { ClassPathXmlApplicationContext bf = newContext("aspects.xml"); - ITestBean tb = (ITestBean) bf.getBean("adrian"); + ITestBean tb = bf.getBean("adrian", ITestBean.class); assertThat(tb.getAge()).isEqualTo(68); MethodInvokingFactoryBean factoryBean = (MethodInvokingFactoryBean) bf.getBean("&factoryBean"); assertThat(AopUtils.isAopProxy(factoryBean.getTargetObject())).isTrue(); @@ -96,7 +96,7 @@ void aspectsAreApplied() { void multipleAspectsWithParameterApplied() { ClassPathXmlApplicationContext bf = newContext("aspects.xml"); - ITestBean tb = (ITestBean) bf.getBean("adrian"); + ITestBean tb = bf.getBean("adrian", ITestBean.class); tb.setAge(10); assertThat(tb.getAge()).isEqualTo(20); } @@ -105,7 +105,7 @@ void multipleAspectsWithParameterApplied() { void aspectsAreAppliedInDefinedOrder() { ClassPathXmlApplicationContext bf = newContext("aspectsWithOrdering.xml"); - ITestBean tb = (ITestBean) bf.getBean("adrian"); + ITestBean tb = bf.getBean("adrian", ITestBean.class); assertThat(tb.getAge()).isEqualTo(71); } @@ -113,8 +113,8 @@ void aspectsAreAppliedInDefinedOrder() { void aspectsAndAdvisorAreApplied() { ClassPathXmlApplicationContext ac = newContext("aspectsPlusAdvisor.xml"); - ITestBean shouldBeWeaved = (ITestBean) ac.getBean("adrian"); - doTestAspectsAndAdvisorAreApplied(ac, shouldBeWeaved); + ITestBean shouldBeWoven = ac.getBean("adrian", ITestBean.class); + assertAspectsAndAdvisorAreApplied(ac, shouldBeWoven); } @Test @@ -124,20 +124,22 @@ void aspectsAndAdvisorAreAppliedEvenIfComingFromParentFactory() { GenericApplicationContext childAc = new GenericApplicationContext(ac); // Create a child factory with a bean that should be woven RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); - bd.getPropertyValues().addPropertyValue(new PropertyValue("name", "Adrian")) + bd.getPropertyValues() + .addPropertyValue(new PropertyValue("name", "Adrian")) .addPropertyValue(new PropertyValue("age", 34)); childAc.registerBeanDefinition("adrian2", bd); // Register the advisor auto proxy creator with subclass - childAc.registerBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class.getName(), new RootBeanDefinition( - AnnotationAwareAspectJAutoProxyCreator.class)); + childAc.registerBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class.getName(), + new RootBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class)); childAc.refresh(); - ITestBean beanFromChildContextThatShouldBeWeaved = (ITestBean) childAc.getBean("adrian2"); - //testAspectsAndAdvisorAreApplied(childAc, (ITestBean) ac.getBean("adrian")); - doTestAspectsAndAdvisorAreApplied(childAc, beanFromChildContextThatShouldBeWeaved); + ITestBean beanFromParentContextThatShouldBeWoven = ac.getBean("adrian", ITestBean.class); + ITestBean beanFromChildContextThatShouldBeWoven = childAc.getBean("adrian2", ITestBean.class); + assertAspectsAndAdvisorAreApplied(childAc, beanFromParentContextThatShouldBeWoven); + assertAspectsAndAdvisorAreApplied(childAc, beanFromChildContextThatShouldBeWoven); } - protected void doTestAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBean shouldBeWeaved) { + protected void assertAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBean shouldBeWoven) { TestBeanAdvisor tba = (TestBeanAdvisor) ac.getBean("advisor"); MultiplyReturnValue mrv = (MultiplyReturnValue) ac.getBean("aspect"); @@ -146,10 +148,10 @@ protected void doTestAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBea tba.count = 0; mrv.invocations = 0; - assertThat(AopUtils.isAopProxy(shouldBeWeaved)).as("Autoproxying must apply from @AspectJ aspect").isTrue(); - assertThat(shouldBeWeaved.getName()).isEqualTo("Adrian"); + assertThat(AopUtils.isAopProxy(shouldBeWoven)).as("Autoproxying must apply from @AspectJ aspect").isTrue(); + assertThat(shouldBeWoven.getName()).isEqualTo("Adrian"); assertThat(mrv.invocations).isEqualTo(0); - assertThat(shouldBeWeaved.getAge()).isEqualTo((34 * mrv.getMultiple())); + assertThat(shouldBeWoven.getAge()).isEqualTo((34 * mrv.getMultiple())); assertThat(tba.count).as("Spring advisor must be invoked").isEqualTo(2); assertThat(mrv.invocations).as("Must be able to hold state in aspect").isEqualTo(1); } @@ -158,13 +160,13 @@ protected void doTestAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBea void perThisAspect() { ClassPathXmlApplicationContext bf = newContext("perthis.xml"); - ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian1 = bf.getBean("adrian", ITestBean.class); assertThat(AopUtils.isAopProxy(adrian1)).isTrue(); assertThat(adrian1.getAge()).isEqualTo(0); assertThat(adrian1.getAge()).isEqualTo(1); - ITestBean adrian2 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian2 = bf.getBean("adrian", ITestBean.class); assertThat(adrian2).isNotSameAs(adrian1); assertThat(AopUtils.isAopProxy(adrian1)).isTrue(); assertThat(adrian2.getAge()).isEqualTo(0); @@ -178,7 +180,7 @@ void perThisAspect() { void perTargetAspect() throws SecurityException, NoSuchMethodException { ClassPathXmlApplicationContext bf = newContext("pertarget.xml"); - ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian1 = bf.getBean("adrian", ITestBean.class); assertThat(AopUtils.isAopProxy(adrian1)).isTrue(); // Does not trigger advice or count @@ -199,7 +201,7 @@ void perTargetAspect() throws SecurityException, NoSuchMethodException { adrian1.setName("Adrian"); //assertEquals("Any other setter does not increment", 2, adrian1.getAge()); - ITestBean adrian2 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian2 = bf.getBean("adrian", ITestBean.class); assertThat(adrian2).isNotSameAs(adrian1); assertThat(AopUtils.isAopProxy(adrian1)).isTrue(); assertThat(adrian2.getAge()).isEqualTo(34); @@ -239,7 +241,7 @@ void cglibProxyClassIsCachedAcrossApplicationContextsForPerTargetAspect() { void twoAdviceAspect() { ClassPathXmlApplicationContext bf = newContext("twoAdviceAspect.xml"); - ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian1 = bf.getBean("adrian", ITestBean.class); testAgeAspect(adrian1, 0, 2); } @@ -247,9 +249,9 @@ void twoAdviceAspect() { void twoAdviceAspectSingleton() { ClassPathXmlApplicationContext bf = newContext("twoAdviceAspectSingleton.xml"); - ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian1 = bf.getBean("adrian", ITestBean.class); testAgeAspect(adrian1, 0, 1); - ITestBean adrian2 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian2 = bf.getBean("adrian", ITestBean.class); assertThat(adrian2).isNotSameAs(adrian1); testAgeAspect(adrian2, 2, 1); } @@ -258,9 +260,9 @@ void twoAdviceAspectSingleton() { void twoAdviceAspectPrototype() { ClassPathXmlApplicationContext bf = newContext("twoAdviceAspectPrototype.xml"); - ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian1 = bf.getBean("adrian", ITestBean.class); testAgeAspect(adrian1, 0, 1); - ITestBean adrian2 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian2 = bf.getBean("adrian", ITestBean.class); assertThat(adrian2).isNotSameAs(adrian1); testAgeAspect(adrian2, 0, 1); } @@ -280,7 +282,7 @@ private void testAgeAspect(ITestBean adrian, int start, int increment) { void adviceUsingJoinPoint() { ClassPathXmlApplicationContext bf = newContext("usesJoinPointAspect.xml"); - ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + ITestBean adrian1 = bf.getBean("adrian", ITestBean.class); adrian1.getAge(); AdviceUsingThisJoinPoint aspectInstance = (AdviceUsingThisJoinPoint) bf.getBean("aspect"); //(AdviceUsingThisJoinPoint) Aspects.aspectOf(AdviceUsingThisJoinPoint.class); @@ -292,7 +294,7 @@ void adviceUsingJoinPoint() { void includeMechanism() { ClassPathXmlApplicationContext bf = newContext("usesInclude.xml"); - ITestBean adrian = (ITestBean) bf.getBean("adrian"); + ITestBean adrian = bf.getBean("adrian", ITestBean.class); assertThat(AopUtils.isAopProxy(adrian)).isTrue(); assertThat(adrian.getAge()).isEqualTo(68); } @@ -310,7 +312,7 @@ void forceProxyTargetClass() { void withAbstractFactoryBeanAreApplied() { ClassPathXmlApplicationContext bf = newContext("aspectsWithAbstractBean.xml"); - ITestBean adrian = (ITestBean) bf.getBean("adrian"); + ITestBean adrian = bf.getBean("adrian", ITestBean.class); assertThat(AopUtils.isAopProxy(adrian)).isTrue(); assertThat(adrian.getAge()).isEqualTo(68); } @@ -321,8 +323,7 @@ void retryAspect() { UnreliableBean bean = (UnreliableBean) bf.getBean("unreliableBean"); RetryAspect aspect = (RetryAspect) bf.getBean("retryAspect"); - int attempts = bean.unreliable(); - assertThat(attempts).isEqualTo(2); + assertThat(bean.unreliable()).isEqualTo(2); assertThat(aspect.getBeginCalls()).isEqualTo(2); assertThat(aspect.getRollbackCalls()).isEqualTo(1); assertThat(aspect.getCommitCalls()).isEqualTo(1); @@ -332,7 +333,7 @@ void retryAspect() { void withBeanNameAutoProxyCreator() { ClassPathXmlApplicationContext bf = newContext("withBeanNameAutoProxyCreator.xml"); - ITestBean tb = (ITestBean) bf.getBean("adrian"); + ITestBean tb = bf.getBean("adrian", ITestBean.class); assertThat(tb.getAge()).isEqualTo(68); } @@ -363,6 +364,16 @@ void lambdaIsAlwaysProxiedWithJdkProxyWithIntroductions(Class configClass) { } } + @Test + void nullAdviceIsSkipped() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(ProxyWithNullAdviceConfig.class)) { + @SuppressWarnings("unchecked") + Supplier supplier = context.getBean(Supplier.class); + assertThat(AopUtils.isAopProxy(supplier)).as("AOP proxy").isTrue(); + assertThat(supplier.get()).isEqualTo("lambda"); + } + } + private ClassPathXmlApplicationContext newContext(String fileSuffix) { return new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-" + fileSuffix, getClass()); } @@ -609,7 +620,7 @@ SupplierAdvice supplierAdvice() { @Aspect static class SupplierAdvice { - @Around("execution(public * org.springframework.aop.aspectj.autoproxy..*.*(..))") + @Around("execution(* java.util.function.Supplier+.get())") Object aroundSupplier(ProceedingJoinPoint joinPoint) throws Throwable { return "advised: " + joinPoint.proceed(); } @@ -626,6 +637,16 @@ class ProxyTargetClassFalseConfig extends AbstractProxyTargetClassConfig { class ProxyTargetClassTrueConfig extends AbstractProxyTargetClassConfig { } +@Configuration(proxyBeanMethods = false) +@EnableAspectJAutoProxy(proxyTargetClass = true) +class ProxyWithNullAdviceConfig extends AbstractProxyTargetClassConfig { + + @Override + SupplierAdvice supplierAdvice() { + return null; + } +} + @Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) class PerTargetProxyTargetClassTrueConfig { diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java index 6012b24ab64e..029a66e786cc 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java index ae6c1c7b3b1e..ce718f557ebd 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,19 +48,19 @@ void setup() { @Test - void testAnnotationBindingInAroundAdvice() { + void annotationBindingInAroundAdvice() { assertThat(testBean.doThis()).isEqualTo("this value doThis"); assertThat(testBean.doThat()).isEqualTo("that value doThat"); assertThat(testBean.doArray()).hasSize(2); } @Test - void testNoMatchingWithoutAnnotationPresent() { + void noMatchingWithoutAnnotationPresent() { assertThat(testBean.doTheOther()).isEqualTo("doTheOther"); } @Test - void testPointcutEvaluatedAgainstArray() { + void pointcutEvaluatedAgainstArray() { ctx.getBean("arrayFactoryBean"); } diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/TestAnnotation.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/TestAnnotation.java index 6b655bcc6532..b01ac74e2b27 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/TestAnnotation.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/TestAnnotation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests.java index 69793e8ee3c7..df0268e75703 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ /** * Integration tests for AspectJ auto proxying. Includes mixing with Spring AOP - * Advisors to demonstrate that existing autoproxying contract is honoured. + * Advisors to demonstrate that existing autoproxying contract is honored. * * @author Rod Johnson * @author Chris Beams diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.java index 9da8d7ed3b0a..b12186199209 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests.java index 5dc37ff37f1a..5959ec8f46d2 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests.java index f05f8e018fb6..eae87f550b2a 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 @@ class GenericBridgeMethodMatchingClassProxyTests extends GenericBridgeMethodMatchingTests { @Test - void testGenericDerivedInterfaceMethodThroughClass() { + void genericDerivedInterfaceMethodThroughClass() { ((DerivedStringParameterizedClass) testBean).genericDerivedInterfaceMethod(""); assertThat(counterAspect.count).isEqualTo(1); } @Test - void testGenericBaseInterfaceMethodThroughClass() { + void genericBaseInterfaceMethodThroughClass() { ((DerivedStringParameterizedClass) testBean).genericBaseInterfaceMethod(""); assertThat(counterAspect.count).isEqualTo(1); } diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests.java index e4d2a1feb722..513659388069 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,13 +68,13 @@ void tearDown() { @Test - void testGenericDerivedInterfaceMethodThroughInterface() { + void genericDerivedInterfaceMethodThroughInterface() { testBean.genericDerivedInterfaceMethod(""); assertThat(counterAspect.count).isEqualTo(1); } @Test - void testGenericBaseInterfaceMethodThroughInterface() { + void genericBaseInterfaceMethodThroughInterface() { testBean.genericBaseInterfaceMethod(""); assertThat(counterAspect.count).isEqualTo(1); } diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests.java index 40de9ac41b8b..36065b40b592 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,19 +61,19 @@ void tearDown() { @Test - void testGenericInterfaceGenericArgExecution() { + void genericInterfaceGenericArgExecution() { testBean.save(""); assertThat(counterAspect.genericInterfaceGenericArgExecutionCount).isEqualTo(1); } @Test - void testGenericInterfaceGenericCollectionArgExecution() { + void genericInterfaceGenericCollectionArgExecution() { testBean.saveAll(null); assertThat(counterAspect.genericInterfaceGenericCollectionArgExecutionCount).isEqualTo(1); } @Test - void testGenericInterfaceSubtypeGenericCollectionArgExecution() { + void genericInterfaceSubtypeGenericCollectionArgExecution() { testBean.saveAll(null); assertThat(counterAspect.genericInterfaceSubtypeGenericCollectionArgExecutionCount).isEqualTo(1); } diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests.java index b74ffec0f1a2..fdff6f8814ce 100644 --- a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests.java +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,12 +31,12 @@ class AopNamespaceHandlerAdviceTypeTests { @Test - void testParsingOfAdviceTypes() { + void parsingOfAdviceTypes() { new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); } @Test - void testParsingOfAdviceTypesWithError() { + void parsingOfAdviceTypesWithError() { assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-error.xml", getClass())) .matches(ex -> ex.contains(SAXParseException.class)); diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests.java index 946fb953a458..89dbec507f52 100644 --- a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests.java +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,12 +30,12 @@ class AopNamespaceHandlerArgNamesTests { @Test - void testArgNamesOK() { + void argNamesOK() { new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); } @Test - void testArgNamesError() { + void argNamesError() { assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-error.xml", getClass())) .matches(ex -> ex.contains(IllegalArgumentException.class)); diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests.java index aa7fba9edad3..cfd20662d6a3 100644 --- a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests.java +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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 @@ class AopNamespaceHandlerProxyTargetClassTests extends AopNamespaceHandlerTests { @Test - void testIsClassProxy() { + void isClassProxy() { ITestBean bean = getTestBean(); assertThat(AopUtils.isCglibProxy(bean)).as("Should be a CGLIB proxy").isTrue(); assertThat(((Advised) bean).isExposeProxy()).as("Should expose proxy").isTrue(); diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerReturningTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerReturningTests.java index 58b5b2a67de3..2e34cd153a41 100644 --- a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerReturningTests.java +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerReturningTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,12 +31,12 @@ class AopNamespaceHandlerReturningTests { @Test - void testReturningOnReturningAdvice() { + void returningOnReturningAdvice() { new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); } @Test - void testParseReturningOnOtherAdviceType() { + void parseReturningOnOtherAdviceType() { assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-error.xml", getClass())) .matches(ex -> ex.contains(SAXParseException.class)); diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerTests.java index 4ee987365c5d..b7a036119572 100644 --- a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,7 +53,7 @@ protected ITestBean getTestBean() { @Test - void testIsProxy() { + void isProxy() { ITestBean bean = getTestBean(); assertThat(AopUtils.isAopProxy(bean)).as("Bean is not a proxy").isTrue(); @@ -66,7 +66,7 @@ void testIsProxy() { } @Test - void testAdviceInvokedCorrectly() { + void adviceInvokedCorrectly() { CountingBeforeAdvice getAgeCounter = (CountingBeforeAdvice) this.context.getBean("getAgeCounter"); CountingBeforeAdvice getNameCounter = (CountingBeforeAdvice) this.context.getBean("getNameCounter"); @@ -87,7 +87,7 @@ void testAdviceInvokedCorrectly() { } @Test - void testAspectApplied() { + void aspectApplied() { ITestBean bean = getTestBean(); CountingAspectJAdvice advice = (CountingAspectJAdvice) this.context.getBean("countingAdvice"); @@ -107,7 +107,7 @@ void testAspectApplied() { } @Test - void testAspectAppliedForInitializeBeanWithEmptyName() { + void aspectAppliedForInitializeBeanWithEmptyName() { ITestBean bean = (ITestBean) this.context.getAutowireCapableBeanFactory().initializeBean(new TestBean(), ""); CountingAspectJAdvice advice = (CountingAspectJAdvice) this.context.getBean("countingAdvice"); @@ -127,7 +127,7 @@ void testAspectAppliedForInitializeBeanWithEmptyName() { } @Test - void testAspectAppliedForInitializeBeanWithNullName() { + void aspectAppliedForInitializeBeanWithNullName() { ITestBean bean = (ITestBean) this.context.getAutowireCapableBeanFactory().initializeBean(new TestBean(), null); CountingAspectJAdvice advice = (CountingAspectJAdvice) this.context.getBean("countingAdvice"); diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerThrowingTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerThrowingTests.java index 4b79307f76bd..132b60468167 100644 --- a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerThrowingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerThrowingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,12 +31,12 @@ class AopNamespaceHandlerThrowingTests { @Test - void testThrowingOnThrowingAdvice() { + void throwingOnThrowingAdvice() { new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); } @Test - void testParseThrowingOnOtherAdviceType() { + void parseThrowingOnOtherAdviceType() { assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-error.xml", getClass())) .matches(ex -> ex.contains(SAXParseException.class)); diff --git a/spring-context/src/test/java/org/springframework/aop/config/MethodLocatingFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/aop/config/MethodLocatingFactoryBeanTests.java index c3f7af3662ee..5963b6302741 100644 --- a/spring-context/src/test/java/org/springframework/aop/config/MethodLocatingFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/aop/config/MethodLocatingFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,24 +40,24 @@ class MethodLocatingFactoryBeanTests { @Test - void testIsSingleton() { + void isSingleton() { assertThat(factory.isSingleton()).isTrue(); } @Test - void testGetObjectType() { + void getObjectType() { assertThat(factory.getObjectType()).isEqualTo(Method.class); } @Test - void testWithNullTargetBeanName() { + void withNullTargetBeanName() { factory.setMethodName("toString()"); assertThatIllegalArgumentException().isThrownBy(() -> factory.setBeanFactory(beanFactory)); } @Test - void testWithEmptyTargetBeanName() { + void withEmptyTargetBeanName() { factory.setTargetBeanName(""); factory.setMethodName("toString()"); assertThatIllegalArgumentException().isThrownBy(() -> @@ -65,14 +65,14 @@ void testWithEmptyTargetBeanName() { } @Test - void testWithNullTargetMethodName() { + void withNullTargetMethodName() { factory.setTargetBeanName(BEAN_NAME); assertThatIllegalArgumentException().isThrownBy(() -> factory.setBeanFactory(beanFactory)); } @Test - void testWithEmptyTargetMethodName() { + void withEmptyTargetMethodName() { factory.setTargetBeanName(BEAN_NAME); factory.setMethodName(""); assertThatIllegalArgumentException().isThrownBy(() -> @@ -80,7 +80,7 @@ void testWithEmptyTargetMethodName() { } @Test - void testWhenTargetBeanClassCannotBeResolved() { + void whenTargetBeanClassCannotBeResolved() { factory.setTargetBeanName(BEAN_NAME); factory.setMethodName("toString()"); assertThatIllegalArgumentException().isThrownBy(() -> @@ -90,22 +90,21 @@ void testWhenTargetBeanClassCannotBeResolved() { @Test @SuppressWarnings({ "unchecked", "rawtypes" }) - void testSunnyDayPath() throws Exception { + void sunnyDayPath() throws Exception { given(beanFactory.getType(BEAN_NAME)).willReturn((Class)String.class); factory.setTargetBeanName(BEAN_NAME); factory.setMethodName("toString()"); factory.setBeanFactory(beanFactory); Object result = factory.getObject(); assertThat(result).isNotNull(); - boolean condition = result instanceof Method; - assertThat(condition).isTrue(); + assertThat(result).isInstanceOf(Method.class); Method method = (Method) result; assertThat(method.invoke("Bingo")).isEqualTo("Bingo"); } @Test @SuppressWarnings({ "unchecked", "rawtypes" }) - void testWhereMethodCannotBeResolved() { + void whereMethodCannotBeResolved() { given(beanFactory.getType(BEAN_NAME)).willReturn((Class)String.class); factory.setTargetBeanName(BEAN_NAME); factory.setMethodName("loadOfOld()"); diff --git a/spring-context/src/test/java/org/springframework/aop/config/PrototypeProxyTests.java b/spring-context/src/test/java/org/springframework/aop/config/PrototypeProxyTests.java index 9093cb62a85d..9690fe0dded8 100644 --- a/spring-context/src/test/java/org/springframework/aop/config/PrototypeProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/config/PrototypeProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java index 49e142044fbf..e45ffa5d9923 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -70,7 +71,6 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.testfixture.TimeStamped; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; @@ -233,8 +233,7 @@ void serializableTargetAndAdvice() throws Throwable { try { p2.echo(new IOException()); } - catch (IOException ex) { - + catch (IOException ignored) { } assertThat(cta.getCalls()).isEqualTo(2); } @@ -338,7 +337,7 @@ void targetCanGetProxy() { @Test // Should fail to get proxy as exposeProxy wasn't set to true - public void targetCantGetProxyByDefault() { + void targetCantGetProxyByDefault() { NeedsToSeeProxy et = new NeedsToSeeProxy(); ProxyFactory pf1 = new ProxyFactory(et); assertThat(pf1.isExposeProxy()).isFalse(); @@ -855,8 +854,7 @@ void canPreventCastToAdvisedUsingOpaque() { assertThat(proxied.getAge()).isEqualTo(10); assertThat(mba.getCalls()).isEqualTo(1); - boolean condition = proxied instanceof Advised; - assertThat(condition).as("Cannot be cast to Advised").isFalse(); + assertThat(proxied).as("Cannot be cast to Advised").isNotInstanceOf(Advised.class); } @Test diff --git a/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java index 343633881fa6..626c324079ff 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.ClassFilter; @@ -35,8 +37,6 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.ApplicationContextException; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -422,6 +422,7 @@ void proxyTargetClassInCaseOfNoInterfaces() { } @Test // SPR-13328 + @SuppressWarnings("unchecked") void varargsWithEnumArray() { ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); MyBean proxy = (MyBean) proxyFactory.getProxy(); diff --git a/spring-context/src/test/java/org/springframework/aop/framework/ClassWithComplexConstructor.java b/spring-context/src/test/java/org/springframework/aop/framework/ClassWithComplexConstructor.java index 82dd189b9707..a8ebcf2e211d 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/ClassWithComplexConstructor.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/ClassWithComplexConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/Dependency.java b/spring-context/src/test/java/org/springframework/aop/framework/Dependency.java index c965f3a74165..b98bd458d8bf 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/Dependency.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/Dependency.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/Echo.java b/spring-context/src/test/java/org/springframework/aop/framework/Echo.java index 90c8c322318f..0f72acc09593 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/Echo.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/Echo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/IEcho.java b/spring-context/src/test/java/org/springframework/aop/framework/IEcho.java index cf0bd603ba25..d49197ea5231 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/IEcho.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/IEcho.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java index f2fb4eea74a1..aa2cf8364be4 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; @@ -25,7 +26,6 @@ import org.springframework.beans.testfixture.beans.IOther; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -141,6 +141,7 @@ void equalsAndHashCodeDefined() { } @Test // SPR-13328 + @SuppressWarnings("unchecked") void varargsWithEnumArray() { ProxyFactory proxyFactory = new ProxyFactory(new VarargTestBean()); VarargTestInterface proxy = (VarargTestInterface) proxyFactory.getProxy(); diff --git a/spring-context/src/test/java/org/springframework/aop/framework/ObjenesisProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/ObjenesisProxyTests.java index d50252e323ff..21d7d92be2c5 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/ObjenesisProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/ObjenesisProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java index 481fe6e591ec..d62cc41eb3c1 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -58,7 +59,6 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.testfixture.TimeStamped; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; @@ -102,25 +102,25 @@ void setup() { @Test - void testIsDynamicProxyWhenInterfaceSpecified() { + void isDynamicProxyWhenInterfaceSpecified() { ITestBean test1 = (ITestBean) factory.getBean("test1"); assertThat(Proxy.isProxyClass(test1.getClass())).as("test1 is a dynamic proxy").isTrue(); } @Test - void testIsDynamicProxyWhenInterfaceSpecifiedForPrototype() { + void isDynamicProxyWhenInterfaceSpecifiedForPrototype() { ITestBean test1 = (ITestBean) factory.getBean("test2"); assertThat(Proxy.isProxyClass(test1.getClass())).as("test2 is a dynamic proxy").isTrue(); } @Test - void testIsDynamicProxyWhenAutodetectingInterfaces() { + void isDynamicProxyWhenAutodetectingInterfaces() { ITestBean test1 = (ITestBean) factory.getBean("test3"); assertThat(Proxy.isProxyClass(test1.getClass())).as("test3 is a dynamic proxy").isTrue(); } @Test - void testIsDynamicProxyWhenAutodetectingInterfacesForPrototype() { + void isDynamicProxyWhenAutodetectingInterfacesForPrototype() { ITestBean test1 = (ITestBean) factory.getBean("test4"); assertThat(Proxy.isProxyClass(test1.getClass())).as("test4 is a dynamic proxy").isTrue(); } @@ -130,17 +130,18 @@ void testIsDynamicProxyWhenAutodetectingInterfacesForPrototype() { * interceptor chain and targetSource property. */ @Test - void testDoubleTargetSourcesAreRejected() { - testDoubleTargetSourceIsRejected("doubleTarget"); + void doubleTargetSourcesAreRejected() { + assertDoubleTargetSourceIsRejected("doubleTarget"); // Now with conversion from arbitrary bean to a TargetSource - testDoubleTargetSourceIsRejected("arbitraryTarget"); + assertDoubleTargetSourceIsRejected("arbitraryTarget"); } - private void testDoubleTargetSourceIsRejected(String name) { + private static void assertDoubleTargetSourceIsRejected(String name) { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(DBL_TARGETSOURCE_CONTEXT, CLASS)); - assertThatExceptionOfType(BeanCreationException.class).as("Should not allow TargetSource to be specified in interceptorNames as well as targetSource property") + assertThatExceptionOfType(BeanCreationException.class) + .as("Should not allow TargetSource to be specified in interceptorNames as well as targetSource property") .isThrownBy(() -> bf.getBean(name)) .havingCause() .isInstanceOf(AopConfigException.class) @@ -148,7 +149,7 @@ private void testDoubleTargetSourceIsRejected(String name) { } @Test - void testTargetSourceNotAtEndOfInterceptorNamesIsRejected() { + void targetSourceNotAtEndOfInterceptorNamesIsRejected() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(NOTLAST_TARGETSOURCE_CONTEXT, CLASS)); @@ -160,7 +161,7 @@ void testTargetSourceNotAtEndOfInterceptorNamesIsRejected() { } @Test - void testGetObjectTypeWithDirectTarget() { + void getObjectTypeWithDirectTarget() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); @@ -177,7 +178,7 @@ void testGetObjectTypeWithDirectTarget() { } @Test - void testGetObjectTypeWithTargetViaTargetSource() { + void getObjectTypeWithTargetViaTargetSource() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); ITestBean tb = (ITestBean) bf.getBean("viaTargetSource"); @@ -187,7 +188,7 @@ void testGetObjectTypeWithTargetViaTargetSource() { } @Test - void testGetObjectTypeWithNoTargetOrTargetSource() { + void getObjectTypeWithNoTargetOrTargetSource() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); @@ -198,7 +199,7 @@ void testGetObjectTypeWithNoTargetOrTargetSource() { } @Test - void testGetObjectTypeOnUninitializedFactoryBean() { + void getObjectTypeOnUninitializedFactoryBean() { ProxyFactoryBean pfb = new ProxyFactoryBean(); assertThat(pfb.getObjectType()).isNull(); } @@ -208,7 +209,7 @@ void testGetObjectTypeOnUninitializedFactoryBean() { * Interceptors and interfaces and the target are the same. */ @Test - void testSingletonInstancesAreEqual() { + void singletonInstancesAreEqual() { ITestBean test1 = (ITestBean) factory.getBean("test1"); ITestBean test1_1 = (ITestBean) factory.getBean("test1"); //assertTrue("Singleton instances ==", test1 == test1_1); @@ -232,7 +233,7 @@ void testSingletonInstancesAreEqual() { } @Test - void testPrototypeInstancesAreNotEqual() { + void prototypeInstancesAreNotEqual() { assertThat(factory.getType("prototype")).isAssignableTo(ITestBean.class); ITestBean test2 = (ITestBean) factory.getBean("prototype"); ITestBean test2_1 = (ITestBean) factory.getBean("prototype"); @@ -246,7 +247,7 @@ void testPrototypeInstancesAreNotEqual() { * @param beanName name of the ProxyFactoryBean definition that should * be a prototype */ - private Object testPrototypeInstancesAreIndependent(String beanName) { + private static Object assertPrototypeInstancesAreIndependent(String beanName) { // Initial count value set in bean factory XML int INITIAL_COUNT = 10; @@ -276,8 +277,8 @@ private Object testPrototypeInstancesAreIndependent(String beanName) { } @Test - void testCglibPrototypeInstance() { - Object prototype = testPrototypeInstancesAreIndependent("cglibPrototype"); + void cglibPrototypeInstance() { + Object prototype = assertPrototypeInstancesAreIndependent("cglibPrototype"); assertThat(AopUtils.isCglibProxy(prototype)).as("It's a cglib proxy").isTrue(); assertThat(AopUtils.isJdkDynamicProxy(prototype)).as("It's not a dynamic proxy").isFalse(); } @@ -286,7 +287,7 @@ void testCglibPrototypeInstance() { * Test invoker is automatically added to manipulate target. */ @Test - void testAutoInvoker() { + void autoInvoker() { String name = "Hieronymous"; TestBean target = (TestBean) factory.getBean("test"); target.setName(name); @@ -295,7 +296,7 @@ void testAutoInvoker() { } @Test - void testCanGetFactoryReferenceAndManipulate() { + void canGetFactoryReferenceAndManipulate() { ProxyFactoryBean config = (ProxyFactoryBean) factory.getBean("&test1"); assertThat(config.getObjectType()).isAssignableTo(ITestBean.class); assertThat(factory.getType("test1")).isAssignableTo(ITestBean.class); @@ -327,7 +328,7 @@ void testCanGetFactoryReferenceAndManipulate() { * autowire without ambiguity from target and proxy */ @Test - void testTargetAsInnerBean() { + void targetAsInnerBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(INNER_BEAN_TARGET_CONTEXT, CLASS)); ITestBean itb = (ITestBean) bf.getBean("testBean"); @@ -343,7 +344,7 @@ void testTargetAsInnerBean() { * Each instance will be independent. */ @Test - void testCanAddAndRemoveAspectInterfacesOnPrototype() { + void canAddAndRemoveAspectInterfacesOnPrototype() { assertThat(factory.getBean("test2")).as("Shouldn't implement TimeStamped before manipulation") .isNotInstanceOf(TimeStamped.class); @@ -402,7 +403,7 @@ void testCanAddAndRemoveAspectInterfacesOnPrototype() { * singleton. */ @Test - void testCanAddAndRemoveAdvicesOnSingleton() { + void canAddAndRemoveAdvicesOnSingleton() { ITestBean it = (ITestBean) factory.getBean("test1"); Advised pc = (Advised) it; it.getAge(); @@ -415,7 +416,7 @@ void testCanAddAndRemoveAdvicesOnSingleton() { } @Test - void testMethodPointcuts() { + void methodPointcuts() { ITestBean tb = (ITestBean) factory.getBean("pointcuts"); PointcutForVoid.reset(); assertThat(PointcutForVoid.methodNames).as("No methods intercepted").isEmpty(); @@ -430,7 +431,7 @@ void testMethodPointcuts() { } @Test - void testCanAddThrowsAdviceWithoutAdvisor() { + void canAddThrowsAdviceWithoutAdvisor() { DefaultListableBeanFactory f = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(f).loadBeanDefinitions(new ClassPathResource(THROWS_ADVICE_CONTEXT, CLASS)); MyThrowsHandler th = (MyThrowsHandler) f.getBean("throwsAdvice"); @@ -463,19 +464,19 @@ void testCanAddThrowsAdviceWithoutAdvisor() { // TODO put in sep file to check quality of error message /* @Test - void testNoInterceptorNamesWithoutTarget() { + void noInterceptorNamesWithoutTarget() { assertThatExceptionOfType(AopConfigurationException.class).as("Should require interceptor names").isThrownBy(() -> ITestBean tb = (ITestBean) factory.getBean("noInterceptorNamesWithoutTarget")); } @Test - void testNoInterceptorNamesWithTarget() { + void noInterceptorNamesWithTarget() { ITestBean tb = (ITestBean) factory.getBean("noInterceptorNamesWithoutTarget"); } */ @Test - void testEmptyInterceptorNames() { + void emptyInterceptorNames() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(INVALID_CONTEXT, CLASS)); assertThat(bf.getBean("emptyInterceptorNames")).isInstanceOf(ITestBean.class); @@ -486,7 +487,7 @@ void testEmptyInterceptorNames() { * Globals must be followed by a target. */ @Test - void testGlobalsWithoutTarget() { + void globalsWithoutTarget() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(INVALID_CONTEXT, CLASS)); assertThatExceptionOfType(BeanCreationException.class).as("Should require target name").isThrownBy(() -> @@ -501,7 +502,7 @@ void testGlobalsWithoutTarget() { * to be included in proxiedInterface []. */ @Test - void testGlobalsCanAddAspectInterfaces() { + void globalsCanAddAspectInterfaces() { AddedGlobalInterface agi = (AddedGlobalInterface) factory.getBean("autoInvoker"); assertThat(agi.globalsAdded()).isEqualTo(-1); @@ -520,7 +521,7 @@ void testGlobalsCanAddAspectInterfaces() { } @Test - void testSerializableSingletonProxy() throws Exception { + void serializableSingletonProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); Person p = (Person) bf.getBean("serializableSingleton"); @@ -543,7 +544,7 @@ void testSerializableSingletonProxy() throws Exception { } @Test - void testSerializablePrototypeProxy() throws Exception { + void serializablePrototypeProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); Person p = (Person) bf.getBean("serializablePrototype"); @@ -555,7 +556,7 @@ void testSerializablePrototypeProxy() throws Exception { } @Test - void testSerializableSingletonProxyFactoryBean() throws Exception { + void serializableSingletonProxyFactoryBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); Person p = (Person) bf.getBean("serializableSingleton"); @@ -568,7 +569,7 @@ void testSerializableSingletonProxyFactoryBean() throws Exception { } @Test - void testProxyNotSerializableBecauseOfAdvice() throws Exception { + void proxyNotSerializableBecauseOfAdvice() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); Person p = (Person) bf.getBean("interceptorNotSerializableSingleton"); @@ -576,7 +577,7 @@ void testProxyNotSerializableBecauseOfAdvice() throws Exception { } @Test - void testPrototypeAdvisor() { + void prototypeAdvisor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(CONTEXT, CLASS)); @@ -597,7 +598,7 @@ void testPrototypeAdvisor() { } @Test - void testPrototypeInterceptorSingletonTarget() { + void prototypeInterceptorSingletonTarget() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(CONTEXT, CLASS)); @@ -622,14 +623,14 @@ void testPrototypeInterceptorSingletonTarget() { * Checks for correct use of getType() by bean factory. */ @Test - void testInnerBeanTargetUsingAutowiring() { + void innerBeanTargetUsingAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(AUTOWIRING_CONTEXT, CLASS)); bf.getBean("testBean"); } @Test - void testFrozenFactoryBean() { + void frozenFactoryBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(FROZEN_CONTEXT, CLASS)); @@ -638,7 +639,7 @@ void testFrozenFactoryBean() { } @Test - void testDetectsInterfaces() { + void detectsInterfaces() { ProxyFactoryBean fb = new ProxyFactoryBean(); fb.setTarget(new TestBean()); fb.addAdvice(new DebugInterceptor()); @@ -649,7 +650,7 @@ void testDetectsInterfaces() { } @Test - void testWithInterceptorNames() { + void withInterceptorNames() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("debug", new DebugInterceptor()); @@ -663,7 +664,7 @@ void testWithInterceptorNames() { } @Test - void testWithLateInterceptorNames() { + void withLateInterceptorNames() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("debug", new DebugInterceptor()); diff --git a/spring-context/src/test/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests.java b/spring-context/src/test/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests.java index 30a45d913bd7..90c116f94ffb 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java index a174a3392d79..6505c979ee06 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java index 90731091f331..ea238c54bff1 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.TargetSource; @@ -45,7 +46,6 @@ import org.springframework.context.MessageSource; import org.springframework.context.support.StaticApplicationContext; import org.springframework.context.support.StaticMessageSource; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -414,8 +414,7 @@ public void setProxyObject(boolean proxyObject) { } @Override - @Nullable - protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String name, @Nullable TargetSource customTargetSource) { + protected Object @Nullable [] getAdvicesAndAdvisorsForBean(Class beanClass, String name, @Nullable TargetSource customTargetSource) { if (StaticMessageSource.class.equals(beanClass)) { return DO_NOT_PROXY; } diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java index 799cbf11e05a..008bbd169f8b 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.MethodBeforeAdvice; import org.springframework.beans.testfixture.beans.Pet; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java index 836f84927668..ba322a6c168e 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreatorTests.java new file mode 100644 index 000000000000..959ac8dff025 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreatorTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 java.lang.reflect.Method; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.RootClassFilter; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link DefaultAdvisorAutoProxyCreator}. + * + * @author Sam Brannen + * @since 6.2.1 + */ +class DefaultAdvisorAutoProxyCreatorTests { + + /** + * Indirectly tests behavior of {@link org.springframework.aop.framework.AdvisedSupport.MethodCacheKey}. + * @see StaticMethodMatcherPointcut#matches(Method, Class) + */ + @Test // gh-33915 + void staticMethodMatcherPointcutMatchesMethodIsNotInvokedAgainForActualMethodInvocation() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + DemoBean.class, DemoPointcutAdvisor.class, DefaultAdvisorAutoProxyCreator.class); + DemoPointcutAdvisor demoPointcutAdvisor = context.getBean(DemoPointcutAdvisor.class); + DemoBean demoBean = context.getBean(DemoBean.class); + + assertThat(demoPointcutAdvisor.matchesInvocationCount).as("matches() invocations before").isEqualTo(2); + // Invoke multiple times to ensure additional invocations don't affect the outcome. + assertThat(demoBean.sayHello()).isEqualTo("Advised: Hello!"); + assertThat(demoBean.sayHello()).isEqualTo("Advised: Hello!"); + assertThat(demoBean.sayHello()).isEqualTo("Advised: Hello!"); + assertThat(demoPointcutAdvisor.matchesInvocationCount).as("matches() invocations after").isEqualTo(2); + + context.close(); + } + + + static class DemoBean { + + public String sayHello() { + return "Hello!"; + } + } + + @SuppressWarnings("serial") + static class DemoPointcutAdvisor extends AbstractPointcutAdvisor { + + int matchesInvocationCount = 0; + + @Override + public Pointcut getPointcut() { + StaticMethodMatcherPointcut pointcut = new StaticMethodMatcherPointcut() { + + @Override + public boolean matches(Method method, Class targetClass) { + if (method.getName().equals("sayHello")) { + matchesInvocationCount++; + return true; + } + return false; + } + }; + pointcut.setClassFilter(new RootClassFilter(DemoBean.class)); + return pointcut; + } + + @Override + public Advice getAdvice() { + return (MethodInterceptor) invocation -> "Advised: " + invocation.proceed(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/PackageVisibleMethod.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/PackageVisibleMethod.java index 976c3d89173c..0fda0257a164 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/PackageVisibleMethod.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/PackageVisibleMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java b/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java index 91bc65c02bcb..76fc60ad34fd 100644 --- a/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceTests.java b/spring-context/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceTests.java index 18dbf63832d4..b6e59fce8ae4 100644 --- a/spring-context/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceTests.java +++ b/spring-context/src/test/java/org/springframework/aop/target/CommonsPool2TargetSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,7 +71,7 @@ void tearDown() { this.beanFactory.destroySingletons(); } - private void testFunctionality(String name) { + private void assertFunctionality(String name) { SideEffectBean pooled = (SideEffectBean) beanFactory.getBean(name); assertThat(pooled.getCount()).isEqualTo(INITIAL_COUNT); pooled.doWork(); @@ -85,17 +85,17 @@ private void testFunctionality(String name) { } @Test - void testFunctionality() { - testFunctionality("pooled"); + void functionality() { + assertFunctionality("pooled"); } @Test - void testFunctionalityWithNoInterceptors() { - testFunctionality("pooledNoInterceptors"); + void functionalityWithNoInterceptors() { + assertFunctionality("pooledNoInterceptors"); } @Test - void testConfigMixin() { + void configMixin() { SideEffectBean pooled = (SideEffectBean) beanFactory.getBean("pooledWithMixin"); assertThat(pooled.getCount()).isEqualTo(INITIAL_COUNT); PoolingConfig conf = (PoolingConfig) beanFactory.getBean("pooledWithMixin"); @@ -110,7 +110,7 @@ void testConfigMixin() { } @Test - void testTargetSourceSerializableWithoutConfigMixin() throws Exception { + void targetSourceSerializableWithoutConfigMixin() throws Exception { CommonsPool2TargetSource cpts = (CommonsPool2TargetSource) beanFactory.getBean("personPoolTargetSource"); SingletonTargetSource serialized = SerializationTestUtils.serializeAndDeserialize(cpts, SingletonTargetSource.class); @@ -118,22 +118,20 @@ void testTargetSourceSerializableWithoutConfigMixin() throws Exception { } @Test - void testProxySerializableWithoutConfigMixin() throws Exception { + void proxySerializableWithoutConfigMixin() throws Exception { Person pooled = (Person) beanFactory.getBean("pooledPerson"); - boolean condition1 = ((Advised) pooled).getTargetSource() instanceof CommonsPool2TargetSource; - assertThat(condition1).isTrue(); + assertThat(((Advised) pooled).getTargetSource()).isInstanceOf(CommonsPool2TargetSource.class); //((Advised) pooled).setTargetSource(new SingletonTargetSource(new SerializablePerson())); Person serialized = SerializationTestUtils.serializeAndDeserialize(pooled); - boolean condition = ((Advised) serialized).getTargetSource() instanceof SingletonTargetSource; - assertThat(condition).isTrue(); + assertThat(((Advised) serialized).getTargetSource()).isInstanceOf(SingletonTargetSource.class); serialized.setAge(25); assertThat(serialized.getAge()).isEqualTo(25); } @Test - void testHitMaxSize() throws Exception { + void hitMaxSize() throws Exception { int maxSize = 10; CommonsPool2TargetSource targetSource = new CommonsPool2TargetSource(); @@ -164,7 +162,7 @@ void testHitMaxSize() throws Exception { } @Test - void testHitMaxSizeLoadedFromContext() throws Exception { + void hitMaxSizeLoadedFromContext() throws Exception { Advised person = (Advised) beanFactory.getBean("maxSizePooledPerson"); CommonsPool2TargetSource targetSource = (CommonsPool2TargetSource) person.getTargetSource(); @@ -192,7 +190,7 @@ void testHitMaxSizeLoadedFromContext() throws Exception { } @Test - void testSetWhenExhaustedAction() { + void setWhenExhaustedAction() { CommonsPool2TargetSource targetSource = new CommonsPool2TargetSource(); targetSource.setBlockWhenExhausted(true); assertThat(targetSource.isBlockWhenExhausted()).isTrue(); @@ -206,10 +204,8 @@ void referenceIdentityByDefault() throws Exception { Object first = targetSource.getTarget(); Object second = targetSource.getTarget(); - boolean condition1 = first instanceof SerializablePerson; - assertThat(condition1).isTrue(); - boolean condition = second instanceof SerializablePerson; - assertThat(condition).isTrue(); + assertThat(first).isInstanceOf(SerializablePerson.class); + assertThat(second).isInstanceOf(SerializablePerson.class); assertThat(second).isEqualTo(first); targetSource.releaseTarget(first); diff --git a/spring-context/src/test/java/org/springframework/beans/factory/annotation/BridgeMethodAutowiringTests.java b/spring-context/src/test/java/org/springframework/beans/factory/annotation/BridgeMethodAutowiringTests.java index dd54e086cc36..c4d28e10e1c0 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/annotation/BridgeMethodAutowiringTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/annotation/BridgeMethodAutowiringTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java b/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java index 54dc5370f2d5..aa629df40d16 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.springframework.beans.factory.UnsatisfiedDependencyException; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.support.GenericApplicationContext; @@ -39,77 +40,79 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Integration tests for handling JSR-303 {@link jakarta.inject.Qualifier} annotations. + * Integration tests for handling {@link jakarta.inject.Qualifier} annotations. * * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ class InjectAnnotationAutowireContextTests { + private static final String PERSON1 = "person1"; + + private static final String PERSON2 = "person2"; + private static final String JUERGEN = "juergen"; private static final String MARK = "mark"; @Test - void testAutowiredFieldWithSingleNonQualifiedCandidate() { + void autowiredFieldWithSingleNonQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); - context.registerBeanDefinition(JUERGEN, person); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldTestBean.class)); + context.registerBeanDefinition(PERSON1, person); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test - void testAutowiredMethodParameterWithSingleNonQualifiedCandidate() { + void autowiredMethodParameterWithSingleNonQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); - context.registerBeanDefinition(JUERGEN, person); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + context.registerBeanDefinition(PERSON1, person); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test - void testAutowiredConstructorArgumentWithSingleNonQualifiedCandidate() { + void autowiredConstructorArgumentWithSingleNonQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); - context.registerBeanDefinition(JUERGEN, person); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + context.registerBeanDefinition(PERSON1, person); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - context::refresh) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); } @Test - void testAutowiredFieldWithSingleQualifiedCandidate() { + void autowiredFieldWithSingleQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); - context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition(PERSON1, person); context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); @@ -118,15 +121,14 @@ void testAutowiredFieldWithSingleQualifiedCandidate() { } @Test - void testAutowiredMethodParameterWithSingleQualifiedCandidate() { + void autowiredMethodParameterWithSingleQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); - context.registerBeanDefinition(JUERGEN, person); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + context.registerBeanDefinition(PERSON1, person); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedMethodParameterTestBean bean = @@ -135,15 +137,14 @@ void testAutowiredMethodParameterWithSingleQualifiedCandidate() { } @Test - void testAutowiredMethodParameterWithStaticallyQualifiedCandidate() { + void autowiredMethodParameterWithStaticallyQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(QualifiedPerson.class, cavs, null); - context.registerBeanDefinition(JUERGEN, + context.registerBeanDefinition(PERSON1, ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(person, JUERGEN), context, true).getBeanDefinition()); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedMethodParameterTestBean bean = @@ -152,18 +153,17 @@ void testAutowiredMethodParameterWithStaticallyQualifiedCandidate() { } @Test - void testAutowiredMethodParameterWithStaticallyQualifiedCandidateAmongOthers() { + void autowiredMethodParameterWithStaticallyQualifiedCandidateAmongOthers() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); - RootBeanDefinition person = new RootBeanDefinition(QualifiedPerson.class, cavs, null); + RootBeanDefinition person1 = new RootBeanDefinition(QualifiedPerson.class, cavs, null); ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedMethodParameterTestBean bean = @@ -172,15 +172,14 @@ void testAutowiredMethodParameterWithStaticallyQualifiedCandidateAmongOthers() { } @Test - void testAutowiredConstructorArgumentWithSingleQualifiedCandidate() { + void autowiredConstructorArgumentWithSingleQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); - context.registerBeanDefinition(JUERGEN, person); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + context.registerBeanDefinition(PERSON1, person); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedConstructorArgumentTestBean bean = @@ -189,7 +188,7 @@ void testAutowiredConstructorArgumentWithSingleQualifiedCandidate() { } @Test - void testAutowiredFieldWithMultipleNonQualifiedCandidates() { + void autowiredFieldWithMultipleNonQualifiedCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -197,21 +196,20 @@ void testAutowiredFieldWithMultipleNonQualifiedCandidates() { ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test - void testAutowiredMethodParameterWithMultipleNonQualifiedCandidates() { + void autowiredMethodParameterWithMultipleNonQualifiedCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -219,21 +217,20 @@ void testAutowiredMethodParameterWithMultipleNonQualifiedCandidates() { ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test - void testAutowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { + void autowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -241,18 +238,17 @@ void testAutowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - context::refresh) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); } @Test - void testAutowiredFieldResolvesQualifiedCandidate() { + void autowiredFieldResolvesQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -261,10 +257,9 @@ void testAutowiredFieldResolvesQualifiedCandidate() { ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedFieldTestBean bean = (QualifiedFieldTestBean) context.getBean("autowired"); @@ -272,7 +267,7 @@ void testAutowiredFieldResolvesQualifiedCandidate() { } @Test - void testAutowiredMethodParameterResolvesQualifiedCandidate() { + void autowiredMethodParameterResolvesQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -281,10 +276,9 @@ void testAutowiredMethodParameterResolvesQualifiedCandidate() { ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedMethodParameterTestBean bean = @@ -293,7 +287,7 @@ void testAutowiredMethodParameterResolvesQualifiedCandidate() { } @Test - void testAutowiredConstructorArgumentResolvesQualifiedCandidate() { + void autowiredConstructorArgumentResolvesQualifiedCandidate() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -302,10 +296,9 @@ void testAutowiredConstructorArgumentResolvesQualifiedCandidate() { ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedConstructorArgumentTestBean bean = @@ -313,8 +306,18 @@ void testAutowiredConstructorArgumentResolvesQualifiedCandidate() { assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); } + @Test // gh-33345 + void autowiredConstructorArgumentResolvesJakartaNamedCandidate() { + Class testBeanClass = JakartaNamedConstructorArgumentTestBean.class; + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(testBeanClass, JakartaCat.class, JakartaDog.class); + JakartaNamedConstructorArgumentTestBean bean = context.getBean(testBeanClass); + assertThat(bean.getAnimal1().getName()).isEqualTo("Jakarta Tiger"); + assertThat(bean.getAnimal2().getName()).isEqualTo("Jakarta Fido"); + } + @Test - void testAutowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBeanDefinition() { + void autowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -324,10 +327,9 @@ void testAutowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBea ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedFieldWithDefaultValueTestBean bean = @@ -336,7 +338,7 @@ void testAutowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBea } @Test - void testAutowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingValueOnBeanDefinition() { + void autowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -346,21 +348,20 @@ void testAutowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingValu ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test - void testAutowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefinition() { + void autowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -370,10 +371,9 @@ void testAutowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefi ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedFieldWithDefaultValueTestBean bean = @@ -382,7 +382,7 @@ void testAutowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefi } @Test - void testAutowiredFieldResolvesWithMultipleQualifierValues() { + void autowiredFieldResolvesWithMultipleQualifierValues() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -396,10 +396,9 @@ void testAutowiredFieldResolvesWithMultipleQualifierValues() { AutowireCandidateQualifier qualifier2 = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); qualifier2.setAttribute("number", 123); person2.addQualifier(qualifier2); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedFieldWithMultipleAttributesTestBean bean = @@ -408,7 +407,7 @@ void testAutowiredFieldResolvesWithMultipleQualifierValues() { } @Test - void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictingDefaultValue() { + void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictingDefaultValue() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -423,21 +422,20 @@ void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictingDe qualifier2.setAttribute("number", 123); qualifier2.setAttribute("value", "not the default"); person2.addQualifier(qualifier2); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test - void testAutowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValue() { + void autowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValue() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -452,10 +450,9 @@ void testAutowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValu qualifier2.setAttribute("number", 123); qualifier2.setAttribute("value", "default"); person2.addQualifier(qualifier2); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); context.refresh(); QualifiedFieldWithMultipleAttributesTestBean bean = @@ -464,7 +461,7 @@ void testAutowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValu } @Test - void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMatchingCandidates() { + void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMatchingCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); @@ -479,38 +476,37 @@ void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMatch qualifier2.setAttribute("number", 123); qualifier2.setAttribute("value", "default"); person2.addQualifier(qualifier2); - context.registerBeanDefinition(JUERGEN, person1); - context.registerBeanDefinition(MARK, person2); - context.registerBeanDefinition("autowired", - new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + context.registerBeanDefinition(PERSON1, person1); + context.registerBeanDefinition(PERSON2, person2); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test - void testAutowiredFieldDoesNotResolveWithBaseQualifierAndNonDefaultValueAndMultipleMatchingCandidates() { + void autowiredConstructorArgumentDoesNotResolveWithBaseQualifierAndNonDefaultValueAndMultipleMatchingCandidates() { GenericApplicationContext context = new GenericApplicationContext(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue("the real juergen"); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); - person1.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "juergen")); + person1.addQualifier(new AutowireCandidateQualifier(Qualifier.class, JUERGEN)); ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue("juergen imposter"); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); - person2.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "juergen")); + person2.addQualifier(new AutowireCandidateQualifier(Qualifier.class, JUERGEN)); context.registerBeanDefinition("juergen1", person1); context.registerBeanDefinition("juergen2", person2); context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - context::refresh) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); } @@ -543,7 +539,7 @@ public Person getPerson() { private static class QualifiedConstructorArgumentTestBean { - private Person person; + private final Person person; @Inject public QualifiedConstructorArgumentTestBean(@TestQualifier Person person) { @@ -557,6 +553,29 @@ public Person getPerson() { } + static class JakartaNamedConstructorArgumentTestBean { + + private final Animal animal1; + private final Animal animal2; + + @jakarta.inject.Inject + public JakartaNamedConstructorArgumentTestBean(@jakarta.inject.Named("Cat") Animal animal1, + @jakarta.inject.Named("Dog") Animal animal2) { + + this.animal1 = animal1; + this.animal2 = animal2; + } + + public Animal getAnimal1() { + return this.animal1; + } + + public Animal getAnimal2() { + return this.animal2; + } + } + + public static class QualifiedFieldWithDefaultValueTestBean { @Inject @@ -593,13 +612,13 @@ public Person getPerson() { } - public static class QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean { + static class QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean { private Person person; @Inject public QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean( - @Named("juergen") Person person) { + @Named(JUERGEN) Person person) { this.person = person; } @@ -636,6 +655,32 @@ public QualifiedPerson(String name) { } + interface Animal { + + String getName(); + } + + + @jakarta.inject.Named("Cat") + static class JakartaCat implements Animal { + + @Override + public String getName() { + return "Jakarta Tiger"; + } + } + + + @jakarta.inject.Named("Dog") + static class JakartaDog implements Animal { + + @Override + public String getName() { + return "Jakarta Fido"; + } + } + + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier diff --git a/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java b/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java index 01117baede55..174d03150c33 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests.java index 153f0f515a0e..8e69d52afdfc 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ void setUp() { } @Test - void testAutoProxiedLookup() { + void autoProxiedLookup() { OverloadLookup olup = (OverloadLookup) applicationContext.getBean("autoProxiedOverload"); ITestBean jenny = olup.newTestBean(); assertThat(jenny.getName()).isEqualTo("Jenny"); @@ -58,7 +58,7 @@ void testAutoProxiedLookup() { } @Test - void testRegularlyProxiedLookup() { + void regularlyProxiedLookup() { OverloadLookup olup = (OverloadLookup) applicationContext.getBean("regularlyProxiedOverload"); ITestBean jenny = olup.newTestBean(); assertThat(jenny.getName()).isEqualTo("Jenny"); diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java index 575a7fed5c64..72f5cc4e604e 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,7 +53,7 @@ class QualifierAnnotationTests { @Test - void testNonQualifiedFieldFails() { + void nonQualifiedFieldFails() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -65,7 +65,7 @@ void testNonQualifiedFieldFails() { } @Test - void testQualifiedByValue() { + void qualifiedByValue() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -77,7 +77,7 @@ void testQualifiedByValue() { } @Test - void testQualifiedByParentValue() { + void qualifiedByParentValue() { StaticApplicationContext parent = new StaticApplicationContext(); GenericBeanDefinition parentLarry = new GenericBeanDefinition(); parentLarry.setBeanClass(Person.class); @@ -102,7 +102,7 @@ void testQualifiedByParentValue() { } @Test - void testQualifiedByBeanName() { + void qualifiedByBeanName() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -116,7 +116,7 @@ void testQualifiedByBeanName() { } @Test - void testQualifiedByFieldName() { + void qualifiedByFieldName() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -128,7 +128,7 @@ void testQualifiedByFieldName() { } @Test - void testQualifiedByParameterName() { + void qualifiedByParameterName() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -140,7 +140,7 @@ void testQualifiedByParameterName() { } @Test - void testQualifiedByAlias() { + void qualifiedByAlias() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -152,7 +152,7 @@ void testQualifiedByAlias() { } @Test - void testQualifiedByAnnotation() { + void qualifiedByAnnotation() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -164,7 +164,7 @@ void testQualifiedByAnnotation() { } @Test - void testQualifiedByCustomValue() { + void qualifiedByCustomValue() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -176,7 +176,7 @@ void testQualifiedByCustomValue() { } @Test - void testQualifiedByAnnotationValue() { + void qualifiedByAnnotationValue() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -188,7 +188,7 @@ void testQualifiedByAnnotationValue() { } @Test - void testQualifiedByAttributesFailsWithoutCustomQualifierRegistered() { + void qualifiedByAttributesFailsWithoutCustomQualifierRegistered() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -200,7 +200,7 @@ void testQualifiedByAttributesFailsWithoutCustomQualifierRegistered() { } @Test - void testQualifiedByAttributesWithCustomQualifierRegistered() { + void qualifiedByAttributesWithCustomQualifierRegistered() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); @@ -217,7 +217,7 @@ void testQualifiedByAttributesWithCustomQualifierRegistered() { } @Test - void testInterfaceWithOneQualifiedFactoryAndOneQualifiedBean() { + void interfaceWithOneQualifiedFactoryAndOneQualifiedBean() { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerWithExpressionLanguageTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerWithExpressionLanguageTests.java index c3e6e61f6d88..20e51d88635a 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerWithExpressionLanguageTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerWithExpressionLanguageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTestTypes.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTestTypes.java index 06345c821fc4..83370a3e06ca 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTestTypes.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTestTypes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java index 689a09490948..78f47ad86b2f 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -152,8 +152,8 @@ void refToSeparatePrototypeInstances() { assertThat(emmasJenks.getName()).as("Emmas jenks has right name").isEqualTo("Andrew"); assertThat(emmasJenks).as("Emmas doesn't equal new ref").isNotSameAs(xbf.getBean("jenks")); assertThat(georgiasJenks.getName()).as("Georgias jenks has right name").isEqualTo("Andrew"); - assertThat(emmasJenks.equals(georgiasJenks)).as("They are object equal").isTrue(); - assertThat(emmasJenks.equals(xbf.getBean("jenks"))).as("They object equal direct ref").isTrue(); + assertThat(emmasJenks).as("They are object equal").isEqualTo(georgiasJenks); + assertThat(emmasJenks).as("They object equal direct ref").isEqualTo(xbf.getBean("jenks")); } @Test @@ -1321,7 +1321,7 @@ void replaceMethodOverrideWithSetterInjection() { assertThat(dave2.getName()).isEqualTo("David"); assertThat(dave2).isSameAs(dave1); - // Check unadvised behaviour + // Check unadvised behavior String str = "woierowijeiowiej"; assertThat(oom.echo(str)).isEqualTo(str); @@ -1526,7 +1526,7 @@ void primitiveConstructorArray() { new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); ConstructorArrayTestBean bean = (ConstructorArrayTestBean) xbf.getBean("constructorArray"); assertThat(bean.array).isInstanceOf(int[].class); - assertThat(((int[]) bean.array)).hasSize(1); + assertThat((int[]) bean.array).hasSize(1); assertThat(((int[]) bean.array)[0]).isEqualTo(1); } @@ -1536,7 +1536,7 @@ void indexedPrimitiveConstructorArray() { new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); ConstructorArrayTestBean bean = (ConstructorArrayTestBean) xbf.getBean("indexedConstructorArray"); assertThat(bean.array).isInstanceOf(int[].class); - assertThat(((int[]) bean.array)).hasSize(1); + assertThat((int[]) bean.array).hasSize(1); assertThat(((int[]) bean.array)[0]).isEqualTo(1); } @@ -1546,7 +1546,7 @@ void stringConstructorArrayNoType() { new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); ConstructorArrayTestBean bean = (ConstructorArrayTestBean) xbf.getBean("constructorArrayNoType"); assertThat(bean.array).isInstanceOf(String[].class); - assertThat(((String[]) bean.array)).isEmpty(); + assertThat((String[]) bean.array).isEmpty(); } @Test @@ -1557,7 +1557,7 @@ void stringConstructorArrayNoTypeNonLenient() { bd.setLenientConstructorResolution(false); ConstructorArrayTestBean bean = (ConstructorArrayTestBean) xbf.getBean("constructorArrayNoType"); assertThat(bean.array).isInstanceOf(String[].class); - assertThat(((String[]) bean.array)).isEmpty(); + assertThat((String[]) bean.array).isEmpty(); } @Test diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.java index 08a6eddbd627..1e57b7a6da24 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,19 +98,19 @@ void setUp() { @Test - void testSimpleParser() { + void simpleParser() { TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); assertTestBean(bean); } @Test - void testSimpleDecorator() { + void simpleDecorator() { TestBean bean = (TestBean) this.beanFactory.getBean("customisedTestBean"); assertTestBean(bean); } @Test - void testProxyingDecorator() { + void proxyingDecorator() { ITestBean bean = (ITestBean) this.beanFactory.getBean("debuggingTestBean"); assertTestBean(bean); assertThat(AopUtils.isAopProxy(bean)).isTrue(); @@ -120,7 +120,7 @@ void testProxyingDecorator() { } @Test - void testProxyingDecoratorNoInstance() { + void proxyingDecoratorNoInstance() { String[] beanNames = this.beanFactory.getBeanNamesForType(ApplicationListener.class); assertThat(Arrays.asList(beanNames)).contains("debuggingTestBeanNoInstance"); assertThat(this.beanFactory.getType("debuggingTestBeanNoInstance")).isEqualTo(ApplicationListener.class); @@ -131,7 +131,7 @@ void testProxyingDecoratorNoInstance() { } @Test - void testChainedDecorators() { + void chainedDecorators() { ITestBean bean = (ITestBean) this.beanFactory.getBean("chainedTestBean"); assertTestBean(bean); assertThat(AopUtils.isAopProxy(bean)).isTrue(); @@ -142,27 +142,27 @@ void testChainedDecorators() { } @Test - void testDecorationViaAttribute() { + void decorationViaAttribute() { BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("decorateWithAttribute"); assertThat(beanDefinition.getAttribute("objectName")).isEqualTo("foo"); } @Test // SPR-2728 - public void testCustomElementNestedWithinUtilList() { + void customElementNestedWithinUtilList() { List things = (List) this.beanFactory.getBean("list.of.things"); assertThat(things).isNotNull(); assertThat(things).hasSize(2); } @Test // SPR-2728 - public void testCustomElementNestedWithinUtilSet() { + void customElementNestedWithinUtilSet() { Set things = (Set) this.beanFactory.getBean("set.of.things"); assertThat(things).isNotNull(); assertThat(things).hasSize(2); } @Test // SPR-2728 - public void testCustomElementNestedWithinUtilMap() { + void customElementNestedWithinUtilMap() { Map things = (Map) this.beanFactory.getBean("map.of.things"); assertThat(things).isNotNull(); assertThat(things).hasSize(2); diff --git a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java index c67102f7da86..9ea0d396613f 100644 --- a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java +++ b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import reactor.core.publisher.Flux; @@ -43,7 +44,6 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -531,8 +531,7 @@ public MyCacheResolver() { } @Override - @Nullable - protected Collection getCacheNames(CacheOperationInvocationContext context) { + protected @Nullable Collection getCacheNames(CacheOperationInvocationContext context) { String cacheName = (String) context.getArgs()[0]; if (cacheName != null) { return Collections.singleton(cacheName); @@ -606,9 +605,9 @@ public CompletableFuture insertItem(TestBean item) { return CompletableFuture.completedFuture(item); } - @CacheEvict(cacheNames = "itemCache", allEntries = true) - public CompletableFuture clear() { - return CompletableFuture.completedFuture(null); + @CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0") + public CompletableFuture clear() { + return CompletableFuture.completedFuture(1); } } @@ -655,9 +654,9 @@ public Mono insertItem(TestBean item) { return Mono.just(item); } - @CacheEvict(cacheNames = "itemCache", allEntries = true) - public Mono clear() { - return Mono.empty(); + @CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0") + public Mono clear() { + return Mono.just(1); } } @@ -706,9 +705,9 @@ public Flux insertItem(String id, List item) { return Flux.fromIterable(item); } - @CacheEvict(cacheNames = "itemCache", allEntries = true) - public Flux clear() { - return Flux.empty(); + @CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0") + public Flux clear() { + return Flux.just(1); } } diff --git a/spring-context/src/test/java/org/springframework/cache/NoOpCacheManagerTests.java b/spring-context/src/test/java/org/springframework/cache/NoOpCacheManagerTests.java index b511b2de05ac..1b1cf47d0b36 100644 --- a/spring-context/src/test/java/org/springframework/cache/NoOpCacheManagerTests.java +++ b/spring-context/src/test/java/org/springframework/cache/NoOpCacheManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,14 +35,14 @@ class NoOpCacheManagerTests { private final CacheManager manager = new NoOpCacheManager(); @Test - void testGetCache() { + void getCache() { Cache cache = this.manager.getCache("bucket"); assertThat(cache).isNotNull(); assertThat(this.manager.getCache("bucket")).isSameAs(cache); } @Test - void testNoOpCache() { + void noOpCache() { String name = createRandomKey(); Cache cache = this.manager.getCache(name); assertThat(cache.getName()).isEqualTo(name); @@ -54,7 +54,7 @@ void testNoOpCache() { } @Test - void testCacheName() { + void cacheName() { String name = "bucket"; assertThat(this.manager.getCacheNames()).doesNotContain(name); this.manager.getCache(name); @@ -62,7 +62,7 @@ void testCacheName() { } @Test - void testCacheCallable() { + void cacheCallable() { String name = createRandomKey(); Cache cache = this.manager.getCache(name); Object returnValue = new Object(); @@ -71,7 +71,7 @@ void testCacheCallable() { } @Test - void testCacheGetCallableFail() { + void cacheGetCallableFail() { Cache cache = this.manager.getCache(createRandomKey()); String key = createRandomKey(); try { diff --git a/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java b/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java index f1120e87c03c..f6af1a5857f0 100644 --- a/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java +++ b/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,7 +443,7 @@ public void multipleCacheConfig() { } - @CacheConfig(cacheNames = "myCache") + @CacheConfig("myCache") private interface CacheConfigIfc { @Cacheable diff --git a/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java b/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java index 5c04f80a519e..0ba7898253cf 100644 --- a/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java +++ b/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,23 +18,31 @@ import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.LoggingCacheErrorHandler; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for annotation-based caching methods that use reactive operators. @@ -51,7 +59,9 @@ class ReactiveCachingTests { LateCacheHitDeterminationConfig.class, LateCacheHitDeterminationWithValueWrapperConfig.class}) void cacheHitDetermination(Class configClass) { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); + + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + configClass, ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); Object key = new Object(); @@ -117,7 +127,9 @@ void cacheHitDetermination(Class configClass) { LateCacheHitDeterminationConfig.class, LateCacheHitDeterminationWithValueWrapperConfig.class}) void fluxCacheDoesntDependOnFirstRequest(Class configClass) { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); + + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + configClass, ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); Object key = new Object(); @@ -135,7 +147,116 @@ void fluxCacheDoesntDependOnFirstRequest(Class configClass) { ctx.close(); } - @CacheConfig(cacheNames = "first") + @Test + void cacheErrorHandlerWithSimpleCacheErrorHandler() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveCacheableService.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + assertThatExceptionOfType(CompletionException.class) + .isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withCauseInstanceOf(UnsupportedOperationException.class); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> service.cacheMono(new Object()).block()); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> service.cacheFlux(new Object()).blockFirst()); + } + + @Test + void cacheErrorHandlerWithSimpleCacheErrorHandlerAndSync() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveSyncCacheableService.class); + ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class); + + assertThatExceptionOfType(CompletionException.class) + .isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withCauseInstanceOf(UnsupportedOperationException.class); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> service.cacheMono(new Object()).block()); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> service.cacheFlux(new Object()).blockFirst()); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandler() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + Long r1 = service.cacheFuture(new Object()).join(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFuture").isEqualTo(0L); + + r1 = service.cacheMono(new Object()).block(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheMono").isEqualTo(1L); + + r1 = service.cacheFlux(new Object()).blockFirst(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndSync() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveSyncCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class); + + Long r1 = service.cacheFuture(new Object()).join(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFuture").isEqualTo(0L); + + r1 = service.cacheMono(new Object()).block(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheMono").isEqualTo(1L); + + r1 = service.cacheFlux(new Object()).blockFirst(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationException() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveFailureCacheableService service = ctx.getBean(ReactiveFailureCacheableService.class); + + assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withMessage(IllegalStateException.class.getName() + ": future service error"); + + StepVerifier.create(service.cacheMono(new Object())) + .expectErrorMessage("mono service error") + .verify(); + + StepVerifier.create(service.cacheFlux(new Object())) + .expectErrorMessage("flux service error") + .verify(); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationExceptionAndSync() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveSyncFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveSyncFailureCacheableService service = ctx.getBean(ReactiveSyncFailureCacheableService.class); + + assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withMessage(IllegalStateException.class.getName() + ": future service error"); + + StepVerifier.create(service.cacheMono(new Object())) + .expectErrorMessage("mono service error") + .verify(); + + StepVerifier.create(service.cacheFlux(new Object())) + .expectErrorMessage("flux service error") + .verify(); + } + + + @CacheConfig("first") static class ReactiveCacheableService { private final AtomicLong counter = new AtomicLong(); @@ -161,6 +282,98 @@ Flux cacheFlux(Object arg) { } + @CacheConfig("first") + static class ReactiveSyncCacheableService { + + private final AtomicLong counter = new AtomicLong(); + + @Cacheable(sync = true) + CompletableFuture cacheFuture(Object arg) { + return CompletableFuture.completedFuture(this.counter.getAndIncrement()); + } + + @Cacheable(sync = true) + Mono cacheMono(Object arg) { + return Mono.defer(() -> Mono.just(this.counter.getAndIncrement())); + } + + @Cacheable(sync = true) + Flux cacheFlux(Object arg) { + return Flux.defer(() -> Flux.just(this.counter.getAndIncrement(), 0L, -1L, -2L, -3L)); + } + } + + + @CacheConfig("first") + static class ReactiveFailureCacheableService { + + private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean(); + + @Cacheable + CompletableFuture cacheFuture(Object arg) { + if (!this.cacheFutureInvoked.compareAndSet(false, true)) { + return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice")); + } + return CompletableFuture.failedFuture(new IllegalStateException("future service error")); + } + + @Cacheable + Mono cacheMono(Object arg) { + if (!this.cacheMonoInvoked.compareAndSet(false, true)) { + return Mono.error(new IllegalStateException("mono service invoked twice")); + } + return Mono.error(new IllegalStateException("mono service error")); + } + + @Cacheable + Flux cacheFlux(Object arg) { + if (!this.cacheFluxInvoked.compareAndSet(false, true)) { + return Flux.error(new IllegalStateException("flux service invoked twice")); + } + return Flux.error(new IllegalStateException("flux service error")); + } + } + + + @CacheConfig(cacheNames = "first") + static class ReactiveSyncFailureCacheableService { + + private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean(); + + @Cacheable(sync = true) + CompletableFuture cacheFuture(Object arg) { + if (!this.cacheFutureInvoked.compareAndSet(false, true)) { + return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice")); + } + return CompletableFuture.failedFuture(new IllegalStateException("future service error")); + } + + @Cacheable(sync = true) + Mono cacheMono(Object arg) { + if (!this.cacheMonoInvoked.compareAndSet(false, true)) { + return Mono.error(new IllegalStateException("mono service invoked twice")); + } + return Mono.error(new IllegalStateException("mono service error")); + } + + @Cacheable(sync = true) + Flux cacheFlux(Object arg) { + if (!this.cacheFluxInvoked.compareAndSet(false, true)) { + return Flux.error(new IllegalStateException("flux service invoked twice")); + } + return Flux.error(new IllegalStateException("flux service error")); + } + } + + @Configuration(proxyBeanMethods = false) @EnableCaching static class EarlyCacheHitDeterminationConfig { @@ -237,4 +450,44 @@ public void put(Object key, @Nullable Object value) { } } + + @Configuration + static class ErrorHandlerCachingConfiguration implements CachingConfigurer { + + @Bean + @Override + public CacheErrorHandler errorHandler() { + return new LoggingCacheErrorHandler(); + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class ExceptionCacheManager { + + @Bean + CacheManager cacheManager() { + return new ConcurrentMapCacheManager("first") { + @Override + protected Cache createConcurrentMapCache(String name) { + return new ConcurrentMapCache(name, isAllowNullValues()) { + @Override + public CompletableFuture retrieve(Object key) { + return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve")); + } + @Override + public CompletableFuture retrieve(Object key, Supplier> valueLoader) { + return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve")); + } + @Override + public void put(Object key, @Nullable Object value) { + throw new UnsupportedOperationException("Test exception on put"); + } + }; + } + }; + } + } + } diff --git a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java index 5a8baa9eb642..0804a840665b 100644 --- a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java +++ b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 org.junit.jupiter.api.Test; import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; import static org.assertj.core.api.Assertions.assertThat; @@ -30,8 +29,8 @@ class ConcurrentMapCacheManagerTests { @Test - void testDynamicMode() { - CacheManager cm = new ConcurrentMapCacheManager(); + void dynamicMode() { + ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager(); Cache cache1 = cm.getCache("c1"); assertThat(cache1).isInstanceOf(ConcurrentMapCache.class); Cache cache1again = cm.getCache("c1"); @@ -65,10 +64,18 @@ void testDynamicMode() { assertThat(cache1.get("key3").get()).isNull(); cache1.evict("key3"); assertThat(cache1.get("key3")).isNull(); + + cm.removeCache("c1"); + assertThat(cm.getCache("c1")).isNotSameAs(cache1); + assertThat(cm.getCache("c2")).isSameAs(cache2); + + cm.resetCaches(); + assertThat(cm.getCache("c1")).isNotSameAs(cache1); + assertThat(cm.getCache("c2")).isNotSameAs(cache2); } @Test - void testStaticMode() { + void staticMode() { ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager("c1", "c2"); Cache cache1 = cm.getCache("c1"); assertThat(cache1).isInstanceOf(ConcurrentMapCache.class); @@ -107,15 +114,28 @@ void testStaticMode() { cm.setAllowNullValues(true); Cache cache1y = cm.getCache("c1"); + Cache cache2y = cm.getCache("c2"); cache1y.put("key3", null); assertThat(cache1y.get("key3").get()).isNull(); cache1y.evict("key3"); assertThat(cache1y.get("key3")).isNull(); + cache2y.put("key4", "value4"); + assertThat(cache2y.get("key4").get()).isEqualTo("value4"); + + cm.removeCache("c1"); + assertThat(cm.getCache("c1")).isNull(); + assertThat(cm.getCache("c2")).isSameAs(cache2y); + assertThat(cache2y.get("key4").get()).isEqualTo("value4"); + + cm.resetCaches(); + assertThat(cm.getCache("c1")).isNull(); + assertThat(cm.getCache("c2")).isSameAs(cache2y); + assertThat(cache2y.get("key4")).isNull(); } @Test - void testChangeStoreByValue() { + void changeStoreByValue() { ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager("c1", "c2"); assertThat(cm.isStoreByValue()).isFalse(); Cache cache1 = cm.getCache("c1"); diff --git a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheTests.java b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheTests.java index 533423e50ea5..0606301935a3 100644 --- a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheTests.java +++ b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,13 +73,13 @@ protected ConcurrentMap getNativeCache() { @Test - void testIsStoreByReferenceByDefault() { + void isStoreByReferenceByDefault() { assertThat(this.cache.isStoreByValue()).isFalse(); } @SuppressWarnings("unchecked") @Test - void testSerializer() { + void serializer() { ConcurrentMapCache serializeCache = createCacheWithStoreByValue(); assertThat(serializeCache.isStoreByValue()).isTrue(); @@ -93,7 +93,7 @@ void testSerializer() { } @Test - void testNonSerializableContent() { + void nonSerializableContent() { ConcurrentMapCache serializeCache = createCacheWithStoreByValue(); assertThatIllegalArgumentException().isThrownBy(() -> @@ -104,7 +104,7 @@ void testNonSerializableContent() { } @Test - void testInvalidSerializedContent() { + void invalidSerializedContent() { ConcurrentMapCache serializeCache = createCacheWithStoreByValue(); String key = createRandomKey(); diff --git a/spring-context/src/test/java/org/springframework/cache/config/AnnotationDrivenCacheConfigTests.java b/spring-context/src/test/java/org/springframework/cache/config/AnnotationDrivenCacheConfigTests.java index 92122bee02eb..10befec1b714 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/AnnotationDrivenCacheConfigTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/AnnotationDrivenCacheConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java b/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java index a0c2d5111d06..aee401bd867e 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ protected ConfigurableApplicationContext getApplicationContext() { } @Test - void testKeyStrategy() { + void keyStrategy() { CacheInterceptor ci = this.ctx.getBean( "org.springframework.cache.interceptor.CacheInterceptor#0", CacheInterceptor.class); assertThat(ci.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator")); @@ -67,7 +67,7 @@ void bothSetOnlyResolverIsUsed() { } @Test - void testCacheErrorHandler() { + void cacheErrorHandler() { CacheInterceptor ci = this.ctx.getBean( "org.springframework.cache.interceptor.CacheInterceptor#0", CacheInterceptor.class); assertThat(ci.getErrorHandler()).isSameAs(this.ctx.getBean("errorHandler", CacheErrorHandler.class)); diff --git a/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java index d1888ec84baa..d8e9e1ff2174 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ protected ConfigurableApplicationContext getApplicationContext() { } @Test - void testKeyStrategy() { + void keyStrategy() { CacheInterceptor bean = this.ctx.getBean("cacheAdviceClass", CacheInterceptor.class); assertThat(bean.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator")); } diff --git a/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceParserTests.java b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceParserTests.java index 82aa8fb62169..e9a82fa717ff 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceParserTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java b/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java index 9421d5a1cd5b..5c7dd9d5cfb1 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java index 0c3b2181eb21..4a393f92af61 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,7 +86,7 @@ private void fooGetSimple(FooService service) { } @Test // gh-31238 - public void cglibProxyClassIsCachedAcrossApplicationContexts() { + void cglibProxyClassIsCachedAcrossApplicationContexts() { ConfigurableApplicationContext ctx; // Round #1 @@ -200,7 +200,7 @@ interface FooService { } - @CacheConfig(cacheNames = "testCache") + @CacheConfig("testCache") static class FooServiceImpl implements FooService { private final AtomicLong counter = new AtomicLong(); diff --git a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java index 8762ec2d41a2..3d10e16a3e63 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,9 +92,13 @@ void multipleCacheManagerBeans() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(MultiCacheManagerConfig.class); assertThatThrownBy(ctx::refresh) - .isInstanceOf(NoUniqueBeanDefinitionException.class) - .hasMessageContaining("no CacheResolver specified and expected a single CacheManager bean, but found 2: [cm1,cm2]") - .hasNoCause(); + .isInstanceOfSatisfying(NoUniqueBeanDefinitionException.class, 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"); + }).hasNoCause(); } @Test diff --git a/spring-context/src/test/java/org/springframework/cache/config/ExpressionCachingIntegrationTests.java b/spring-context/src/test/java/org/springframework/cache/config/ExpressionCachingIntegrationTests.java index 0489d30ba694..6d1e3aab6d53 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/ExpressionCachingIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/ExpressionCachingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class ExpressionCachingIntegrationTests { @Test // SPR-11692 @SuppressWarnings("unchecked") - public void expressionIsCacheBasedOnActualMethod() { + void expressionIsCacheBasedOnActualMethod() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(SharedConfig.class, Spr11692Config.class); diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java index ade3d831b824..de2352ca29f0 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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,15 @@ package org.springframework.cache.interceptor; import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; @@ -39,6 +43,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willThrow; @@ -70,7 +76,6 @@ void setup() { this.simpleService = context.getBean(SimpleService.class); } - @AfterEach void closeContext() { this.context.close(); @@ -83,11 +88,56 @@ void getFail() { willThrow(exception).given(this.cache).get(0L); Object result = this.simpleService.get(0L); - verify(this.errorHandler).handleCacheGetError(exception, cache, 0L); + verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L); verify(this.cache).get(0L); verify(this.cache).put(0L, result); // result of the invocation } + @Test + @SuppressWarnings("unchecked") + void getSyncFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + willThrow(exception).given(this.cache).get(eq(0L), any(Callable.class)); + + Object result = this.simpleService.getSync(0L); + assertThat(result).isEqualTo(0L); + verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L); + verify(this.cache).get(eq(0L), any(Callable.class)); + } + + @Test + void getCompletableFutureFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + willThrow(exception).given(this.cache).retrieve(eq(0L)); + + Object result = this.simpleService.getFuture(0L).join(); + assertThat(result).isEqualTo(0L); + verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L); + verify(this.cache).retrieve(eq(0L)); + } + + @Test + void getMonoFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + willThrow(exception).given(this.cache).retrieve(eq(0L)); + + Object result = this.simpleService.getMono(0L).block(); + assertThat(result).isEqualTo(0L); + verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L); + verify(this.cache).retrieve(eq(0L)); + } + + @Test + void getFluxFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + willThrow(exception).given(this.cache).retrieve(eq(0L)); + + Object result = this.simpleService.getFlux(0L).blockLast(); + assertThat(result).isEqualTo(0L); + verify(this.errorHandler).handleCacheGetError(exception, this.cache, 0L); + verify(this.cache).retrieve(eq(0L)); + } + @Test void getAndPutFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); @@ -211,7 +261,7 @@ public Cache mockCache() { } - @CacheConfig(cacheNames = "test") + @CacheConfig("test") public static class SimpleService { private AtomicLong counter = new AtomicLong(); @@ -220,6 +270,26 @@ public Object get(long id) { return this.counter.getAndIncrement(); } + @Cacheable(sync = true) + public Object getSync(long id) { + return this.counter.getAndIncrement(); + } + + @Cacheable + public CompletableFuture getFuture(long id) { + return CompletableFuture.completedFuture(this.counter.getAndIncrement()); + } + + @Cacheable + public Mono getMono(long id) { + return Mono.just(this.counter.getAndIncrement()); + } + + @Cacheable + public Flux getFlux(long id) { + return Flux.just(this.counter.getAndIncrement(), 0L); + } + @CachePut public Object put(long id) { return this.counter.getAndIncrement(); diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java index 11625dce481e..6f49305609f5 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.Collections; import java.util.Iterator; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; @@ -36,7 +37,6 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -60,14 +60,8 @@ class CacheOperationExpressionEvaluatorTests { private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); - private Collection getOps(String name) { - Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class); - return this.source.getCacheOperations(method, AnnotatedClass.class); - } - - @Test - void testMultipleCachingSource() { + void multipleCachingSource() { Collection ops = getOps("multipleCaching"); assertThat(ops).hasSize(2); Iterator it = ops.iterator(); @@ -82,11 +76,11 @@ void testMultipleCachingSource() { } @Test - void testMultipleCachingEval() { + void multipleCachingEval() { AnnotatedClass target = new AnnotatedClass(); Method method = ReflectionUtils.findMethod( AnnotatedClass.class, "multipleCaching", Object.class, Object.class); - Object[] args = new Object[] {new Object(), new Object()}; + Object[] args = {"arg1", "arg2"}; Collection caches = Collections.singleton(new ConcurrentMapCache("test")); EvaluationContext evalCtx = this.eval.createEvaluationContext(caches, method, args, @@ -144,6 +138,12 @@ void resolveBeanReference() { assertThat(value).isEqualTo(String.class.getName()); } + + private Collection getOps(String name) { + Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class); + return this.source.getCacheOperations(method, AnnotatedClass.class); + } + private EvaluationContext createEvaluationContext(Object result) { return createEvaluationContext(result, null); } @@ -155,7 +155,7 @@ private EvaluationContext createEvaluationContext(Object result, @Nullable BeanF AnnotatedClass target = new AnnotatedClass(); Method method = ReflectionUtils.findMethod( AnnotatedClass.class, "multipleCaching", Object.class, Object.class); - Object[] args = new Object[] {new Object(), new Object()}; + Object[] args = new Object[] {"arg1", "arg2"}; Collection caches = Collections.singleton(new ConcurrentMapCache("test")); return this.eval.createEvaluationContext( caches, method, args, target, target.getClass(), method, result); diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheProxyFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheProxyFactoryBeanTests.java index 9c2b5c1ba3ca..7965dfd9a9c2 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheProxyFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheProxyFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java index c47526d30336..31811b0cd403 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,6 +104,7 @@ void getAndPut() { assertThat(this.cache.get(anotherValue + 100).get()).as("Wrong value for @CachePut key").isEqualTo(anotherValue); } + @Configuration @EnableCaching static class Config implements CachingConfigurer { @@ -121,8 +122,10 @@ public SimpleService simpleService() { } - @CacheConfig(cacheNames = "test") + + @CacheConfig("test") public static class SimpleService { + private AtomicLong counter = new AtomicLong(); /** @@ -144,4 +147,5 @@ public Long getAndPut(long id) { return this.counter.getAndIncrement(); } } + } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java index 4e5cafd54b9d..82c2a96e9ba0 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.Collections; import java.util.concurrent.atomic.AtomicLong; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,7 +38,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.testfixture.cache.CacheTestUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -47,7 +47,7 @@ import static org.springframework.context.testfixture.cache.CacheTestUtils.assertCacheMiss; /** - * Provides various {@link CacheResolver} customisations scenario + * Provides various {@link CacheResolver} customizations scenario * * @author Stephane Nicoll * @since 4.1 @@ -206,7 +206,7 @@ public SimpleService simpleService() { } - @CacheConfig(cacheNames = "default") + @CacheConfig("default") static class SimpleService { private final AtomicLong counter = new AtomicLong(); @@ -260,8 +260,7 @@ private RuntimeCacheResolver(CacheManager cacheManager) { } @Override - @Nullable - protected Collection getCacheNames(CacheOperationInvocationContext context) { + protected @Nullable Collection getCacheNames(CacheOperationInvocationContext context) { String cacheName = (String) context.getArgs()[1]; return Collections.singleton(cacheName); } @@ -275,8 +274,7 @@ private NullCacheResolver(CacheManager cacheManager) { } @Override - @Nullable - protected Collection getCacheNames(CacheOperationInvocationContext context) { + protected @Nullable Collection getCacheNames(CacheOperationInvocationContext context) { return null; } } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java index 187379b39758..44eb2ceb27d2 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/LoggingCacheErrorHandlerTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/LoggingCacheErrorHandlerTests.java index 1351e45587f0..31c48d3b9fac 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/LoggingCacheErrorHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/LoggingCacheErrorHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java index 06ff77d58a4f..8b8e7f22b952 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -130,7 +130,7 @@ void serializedKeys() throws Exception { private Object generateKey(Object[] arguments) { - Method method = ReflectionUtils.findMethod(this.getClass(), "generateKey", Object[].class); + Method method = ReflectionUtils.findMethod(getClass(), "generateKey", Object[].class); return this.generator.generate(this, method, arguments); } diff --git a/spring-context/src/test/java/org/springframework/context/LifecycleContextBean.java b/spring-context/src/test/java/org/springframework/context/LifecycleContextBean.java index b05fcb658b76..5474f98fa384 100644 --- a/spring-context/src/test/java/org/springframework/context/LifecycleContextBean.java +++ b/spring-context/src/test/java/org/springframework/context/LifecycleContextBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AbstractCircularImportDetectionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AbstractCircularImportDetectionTests.java index 5fe8db871345..b87600737ea5 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AbstractCircularImportDetectionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AbstractCircularImportDetectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AggressiveFactoryBeanInstantiationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AggressiveFactoryBeanInstantiationTests.java index 46750e613feb..dad7545b4298 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AggressiveFactoryBeanInstantiationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AggressiveFactoryBeanInstantiationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReaderTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReaderTests.java new file mode 100644 index 000000000000..8d4dd56b5a61 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReaderTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.context.support.GenericApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Yanming Zhou + */ +class AnnotatedBeanDefinitionReaderTests { + + @Test + @SuppressWarnings("unchecked") + void registerBeanWithQualifiers() { + GenericApplicationContext context = new GenericApplicationContext(); + AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(context); + + reader.registerBean(TestBean.class, "primary", Primary.class); + assertThat(context.getBeanDefinition("primary").isPrimary()).isTrue(); + + reader.registerBean(TestBean.class, "fallback", Fallback.class); + assertThat(context.getBeanDefinition("fallback").isFallback()).isTrue(); + + reader.registerBean(TestBean.class, "lazy", Lazy.class); + assertThat(context.getBeanDefinition("lazy").isLazyInit()).isTrue(); + + reader.registerBean(TestBean.class, "customQualifier", CustomQualifier.class); + assertThat(context.getBeanDefinition("customQualifier")) + .isInstanceOfSatisfying(AbstractBeanDefinition.class, abd -> + assertThat(abd.hasQualifier(CustomQualifier.class.getTypeName())).isTrue()); + } + + @Lazy(false) + static class TestBean { + } + + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + @interface CustomQualifier { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java index 884bc07d7869..e7f0f46a4bb0 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,7 @@ import java.util.List; import example.scannable.DefaultNamedComponent; -import example.scannable.JakartaManagedBeanComponent; import example.scannable.JakartaNamedComponent; -import example.scannable.JavaxManagedBeanComponent; -import example.scannable.JavaxNamedComponent; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; @@ -34,7 +31,12 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; +import org.springframework.core.OverridingClassLoader; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Service; @@ -94,6 +96,14 @@ void generateBeanNameForConventionBasedComponentWithConflictingNames() { "myComponent", "myService"); } + @Test // gh-36524 + void generateBeanNameForConventionBasedComponentWithMissingAnnotationAttributeTypeViaAsm() throws Exception { + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + MetadataReaderFactory readerFactory = new SimpleMetadataReaderFactory(classLoader); + MetadataReader reader = readerFactory.getMetadataReader(ConventionBasedComponentWithMissingAnnotationAttributeType.class.getName()); + assertGeneratedName(reader.getAnnotationMetadata(), "myComponent"); + } + @Test void generateBeanNameForComponentWithConflictingNames() { BeanDefinition bd = annotatedBeanDef(ComponentWithMultipleConflictingNames.class); @@ -108,21 +118,6 @@ void generateBeanNameWithJakartaNamedComponent() { assertGeneratedName(JakartaNamedComponent.class, "myJakartaNamedComponent"); } - @Test - void generateBeanNameWithJavaxNamedComponent() { - assertGeneratedName(JavaxNamedComponent.class, "myJavaxNamedComponent"); - } - - @Test - void generateBeanNameWithJakartaManagedBeanComponent() { - assertGeneratedName(JakartaManagedBeanComponent.class, "myJakartaManagedBeanComponent"); - } - - @Test - void generateBeanNameWithJavaxManagedBeanComponent() { - assertGeneratedName(JavaxManagedBeanComponent.class, "myJavaxManagedBeanComponent"); - } - @Test void generateBeanNameWithCustomStereotypeComponent() { assertGeneratedName(DefaultNamedComponent.class, "thoreau"); @@ -168,12 +163,32 @@ void generateBeanNameFromSubStereotypeAnnotationWithStringArrayValueAndExplicitC assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice"); } + @Test // gh-34317, gh-34346 + void generateBeanNameFromStereotypeAnnotationWithStringValueAsExplicitAliasForMetaAnnotationOtherThanComponent() { + assertGeneratedName(StereotypeWithoutExplicitName.class, "annotationBeanNameGeneratorTests.StereotypeWithoutExplicitName"); + } + + @Test // gh-34317, gh-34346 + void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentNameWithBlankName() { + assertGeneratedName(StereotypeWithGeneratedName.class, "annotationBeanNameGeneratorTests.StereotypeWithGeneratedName"); + } + + @Test // gh-34317 + void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentName() { + assertGeneratedName(StereotypeWithExplicitName.class, "explicitName"); + } + private void assertGeneratedName(Class clazz, String expectedName) { BeanDefinition bd = annotatedBeanDef(clazz); assertThat(generateBeanName(bd)).isNotBlank().isEqualTo(expectedName); } + private void assertGeneratedName(AnnotationMetadata annotationMetadata, String expectedName) { + BeanDefinition bd = new AnnotatedGenericBeanDefinition(annotationMetadata); + assertThat(generateBeanName(bd)).isNotBlank().isEqualTo(expectedName); + } + private void assertGeneratedNameIsDefault(Class clazz) { BeanDefinition bd = annotatedBeanDef(clazz); String expectedName = this.beanNameGenerator.buildDefaultBeanName(bd); @@ -210,7 +225,7 @@ static class ComponentWithMultipleConflictingNames { @Retention(RetentionPolicy.RUNTIME) @Component @interface ConventionBasedComponent1 { - // This intentionally convention-based. Please do not add @AliasFor. + // This is intentionally convention-based. Please do not add @AliasFor. // See gh-31093. String value() default ""; } @@ -218,7 +233,7 @@ static class ComponentWithMultipleConflictingNames { @Retention(RetentionPolicy.RUNTIME) @Component @interface ConventionBasedComponent2 { - // This intentionally convention-based. Please do not add @AliasFor. + // This is intentionally convention-based. Please do not add @AliasFor. // See gh-31093. String value() default ""; } @@ -233,6 +248,22 @@ static class ConventionBasedComponentWithDuplicateIdenticalNames { static class ConventionBasedComponentWithMultipleConflictingNames { } + static class FilteredType { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ExampleAnnotation { + + Class value() default Void.class; + + String description() default ""; + } + + @ExampleAnnotation(value = FilteredType.class, description = "optional") + @ConventionBasedComponent1("myComponent") + static class ConventionBasedComponentWithMissingAnnotationAttributeType { + } + @Component private static class AnonymousComponent { } @@ -260,7 +291,7 @@ private static class ComponentFromNonStringMeta { @Target(ElementType.TYPE) @Controller @interface TestRestController { - // This intentionally convention-based. Please do not add @AliasFor. + // This is intentionally convention-based. Please do not add @AliasFor. // See gh-31093. String value() default ""; } @@ -319,7 +350,6 @@ static class ComposedControllerAnnotationWithStringValue { String[] basePackages() default {}; } - @TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice") static class ControllerAdviceClass { } @@ -328,4 +358,76 @@ static class ControllerAdviceClass { static class RestControllerAdviceClass { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface MetaAnnotationWithStringAttribute { + + String attribute() default ""; + } + + /** + * Custom stereotype annotation which has a {@code String value} attribute that + * is explicitly declared as an alias for an attribute in a meta-annotation + * other than {@link Component @Component}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Component + @MetaAnnotationWithStringAttribute + @interface MyStereotype { + + @AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute") + String value() default ""; + } + + @MyStereotype("enigma") + static class StereotypeWithoutExplicitName { + } + + /** + * Custom stereotype annotation which is identical to {@link MyStereotype @MyStereotype} + * except that it has a {@link #name} attribute that is an explicit alias for + * {@link Component#value}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Component + @MetaAnnotationWithStringAttribute + @interface MyNamedStereotype { + + @AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute") + String value() default ""; + + @AliasFor(annotation = Component.class, attribute = "value") + String name() default ""; + } + + @MyNamedStereotype(value = "enigma", name ="explicitName") + static class StereotypeWithExplicitName { + } + + @MyNamedStereotype(value = "enigma") + static class StereotypeWithGeneratedName { + } + + static class FilteringClassLoader extends OverridingClassLoader { + + FilteringClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected boolean isEligibleForOverriding(String className) { + return className.startsWith(AnnotationBeanNameGeneratorTests.class.getName()); + } + + @Override + protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + if (name.contains("Filtered")) { + throw new ClassNotFoundException(name); + } + return super.loadClassForOverriding(name); + } + } + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java index 80f174db288f..ae774623fb93 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.Objects; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aot.hint.MemberCategory; @@ -38,7 +39,6 @@ import org.springframework.context.testfixture.context.annotation.CglibConfiguration; import org.springframework.context.testfixture.context.annotation.LambdaBeanConfiguration; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import static java.lang.String.format; @@ -67,6 +67,21 @@ void scanAndRefresh() { assertThat(beans).hasSize(1); } + @Test + void scanAndRefreshWithFullyQualifiedBeanNames() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setBeanNameGenerator(FullyQualifiedConfigurationBeanNameGenerator.INSTANCE); + context.scan("org.springframework.context.annotation6"); + context.refresh(); + + context.getBean(ConfigForScanning.class.getName()); + context.getBean(ConfigForScanning.class.getName() + ".testBean"); // contributed by ConfigForScanning + context.getBean(ComponentForScanning.class.getName()); + context.getBean(Jsr330NamedForScanning.class.getName()); + Map beans = context.getBeansWithAnnotation(Configuration.class); + assertThat(beans).hasSize(1); + } + @Test void registerAndRefresh() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); @@ -74,11 +89,47 @@ void registerAndRefresh() { context.refresh(); context.getBean("testBean"); - context.getBean("name"); + assertThat(context.getBean("name")).isEqualTo("foo"); + assertThat(context.getBean("prefixName")).isEqualTo("barfoo"); + Map beans = context.getBeansWithAnnotation(Configuration.class); + assertThat(beans).hasSize(2); + } + + @Test + void registerAndRefreshWithFullyQualifiedBeanNames() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setBeanNameGenerator(FullyQualifiedConfigurationBeanNameGenerator.INSTANCE); + context.register(Config.class, NameConfig.class); + context.refresh(); + + context.getBean(Config.class.getName() + ".testBean"); + assertThat(context.getBean(NameConfig.class.getName() + ".name")).isEqualTo("foo"); + assertThat(context.getBean(NameConfig.class.getName() + ".prefixName")).isEqualTo("barfoo"); + assertThat(context.getBean("name")).isEqualTo("foo"); + assertThat(context.getBean("prefixName")).isEqualTo("barfoo"); Map beans = context.getBeansWithAnnotation(Configuration.class); assertThat(beans).hasSize(2); } + @Test + void registerAndRefreshWithOverlappingFullyQualifiedBeanNames() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setBeanNameGenerator(FullyQualifiedConfigurationBeanNameGenerator.INSTANCE); + context.setAllowBeanDefinitionOverriding(false); + context.register(Config.class, NameConfig.class, OtherNameConfig.class); + context.refresh(); + + context.getBean(Config.class.getName() + ".testBean"); + assertThat(context.getBean(NameConfig.class.getName() + ".name")).isEqualTo("foo"); + assertThat(context.getBean(NameConfig.class.getName() + ".prefixName")).isEqualTo("barfoo"); + assertThat(context.getBean(OtherNameConfig.class.getName() + ".name")).isEqualTo("fooX"); + assertThat(context.getBean(OtherNameConfig.class.getName() + ".prefixName")).isEqualTo("barXfooX"); + assertThat(context.getBean("name")).isEqualTo("foo"); + assertThat(context.getBean("prefixName")).isEqualTo("barfoo"); + Map beans = context.getBeansWithAnnotation(Configuration.class); + assertThat(beans).hasSize(3); + } + @Test void getBeansWithAnnotation() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); @@ -534,7 +585,7 @@ void refreshForAotRegisterHintsForCglibProxy() { TypeReference cglibType = TypeReference.of(CglibConfiguration.class.getName() + "$$SpringCGLIB$$0"); assertThat(RuntimeHintsPredicates.reflection().onType(cglibType) .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.DECLARED_FIELDS)) + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS)) .accepts(runtimeHints); assertThat(RuntimeHintsPredicates.reflection().onType(CglibConfiguration.class) .withMemberCategories(MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS)) @@ -598,6 +649,16 @@ static class TwoTestBeanConfig { static class NameConfig { @Bean String name() { return "foo"; } + + @Bean(autowireCandidate = false) String prefixName() { return "bar" + name(); } + } + + @Configuration + static class OtherNameConfig { + + @Bean String name() { return "fooX"; } + + @Bean(autowireCandidate = false) String prefixName() { return "barX" + name(); } } @Configuration diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java index fb3d59b5c593..87789944c075 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AsmCircularImportDetectionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AsmCircularImportDetectionTests.java index a71cff29eb25..e1491ae1c17d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AsmCircularImportDetectionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AsmCircularImportDetectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AutoProxyLazyInitTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AutoProxyLazyInitTests.java index 0f73a07a1fa0..0d61425ca31c 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AutoProxyLazyInitTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AutoProxyLazyInitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java index 8ea9ba0db194..85ff05b6f43a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,28 @@ package org.springframework.context.annotation; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.weaving.LoadTimeWeaverAware; +import org.springframework.core.SpringProperties; import org.springframework.core.testfixture.EnabledForTestGroups; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; @@ -34,18 +48,476 @@ class BackgroundBootstrapTests { @Test - @Timeout(5) + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithUnmanagedThread() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.close(); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithUnmanagedThreads() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.getBean("testBean3", TestBean.class); + ctx.getBean("testBean4", TestBean.class); + ctx.close(); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithLoadTimeWeaverAware() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(LoadTimeWeaverAwareBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.close(); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithStrictLockingFlag() { + SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); + try { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(StrictLockingBeanConfig.class); + assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isSameAs(ctx.getBean("testBean1")); + ctx.close(); + } + finally { + SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null); + } + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithStrictLockingInferred() throws InterruptedException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(InferredLockingBeanConfig.class); + ExecutorService threadPool = Executors.newFixedThreadPool(2); + threadPool.submit(() -> ctx.refresh()); + Thread.sleep(500); + threadPool.submit(() -> ctx.getBean("testBean2")); + Thread.sleep(1000); + assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isSameAs(ctx.getBean("testBean1")); + ctx.close(); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithStrictLockingTurnedOff() throws InterruptedException { + SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, false); + try { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(InferredLockingBeanConfig.class); + ExecutorService threadPool = Executors.newFixedThreadPool(2); + threadPool.submit(() -> ctx.refresh()); + Thread.sleep(500); + threadPool.submit(() -> ctx.getBean("testBean2")); + Thread.sleep(1000); + assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isNull(); + ctx.close(); + } + finally { + SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null); + } + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceAgainstMainThread() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceAgainstMainThreadBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.close(); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceWithBlockingMainThread() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceWithBlockingMainThreadBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceInSameThread() { + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInSameThreadBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceInMultipleThreads() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInMultipleThreadsBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + + @Test + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithCustomExecutor() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class); ctx.getBean("testBean1", TestBean.class); ctx.getBean("testBean2", TestBean.class); ctx.getBean("testBean3", TestBean.class); + ctx.getBean("testBean4", TestBean.class); ctx.close(); } + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCustomExecutorAndStrictLocking() { + SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); + try { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.getBean("testBean3", TestBean.class); + ctx.getBean("testBean4", TestBean.class); + ctx.close(); + } + finally { + SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null); + } + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCustomExecutorAndLazyConfig() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorLazyBeanConfig.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isTrue(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isTrue(); + ctx.close(); + } + + + @Configuration(proxyBeanMethods = false) + static class UnmanagedThreadBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2() { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + } + + + @Configuration(proxyBeanMethods = false) + static class UnmanagedThreadsBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean3, ObjectProvider testBean4) { + new Thread(testBean3::getObject).start(); + new Thread(testBean4::getObject).start(); + new Thread(testBean3::getObject).start(); + new Thread(testBean4::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(TestBean testBean4) { + return new TestBean(testBean4); + } + + @Bean + public TestBean testBean3(TestBean testBean4) { + return new TestBean(testBean4); + } + + @Bean + public FactoryBean testBean4() { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + TestBean testBean = new TestBean(); + return new FactoryBean<>() { + @Override + public TestBean getObject() { + return testBean; + } + @Override + public Class getObjectType() { + return testBean.getClass(); + } + }; + } + } + + + @Configuration(proxyBeanMethods = false) + static class LoadTimeWeaverAwareBeanConfig { + + @Bean + LoadTimeWeaverAware loadTimeWeaverAware(ObjectProvider testBean1) { + Thread thread = new Thread(testBean1::getObject); + thread.start(); + try { + thread.join(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return (loadTimeWeaver -> {}); + } + + @Bean + public TestBean testBean1(TestBean testBean2) { + return new TestBean(testBean2); + } + + @Bean @Lazy + public FactoryBean testBean2() { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + TestBean testBean = new TestBean(); + return new FactoryBean<>() { + @Override + public TestBean getObject() { + return testBean; + } + @Override + public Class getObjectType() { + return testBean.getClass(); + } + }; + } + } + + + @Configuration(proxyBeanMethods = false) + static class StrictLockingBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean("testBean1"); + } + + @Bean + public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) { + return new TestBean((TestBean) beanFactory.getSingleton("testBean1")); + } + } + + + @Configuration(proxyBeanMethods = false) + static class InferredLockingBeanConfig { + + @Bean + public TestBean testBean1() { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean("testBean1"); + } + + @Bean + public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) { + return new TestBean((TestBean) beanFactory.getSingleton("testBean1")); + } + } + + + @Configuration(proxyBeanMethods = false) + static class CircularReferenceAgainstMainThreadBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(TestBean testBean1) { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + } + + + @Configuration(proxyBeanMethods = false) + static class CircularReferenceWithBlockingMainThreadBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean2.getObject()); + } + + @Bean + public TestBean testBean2(ObjectProvider testBean1) { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean1.getObject()); + } + } + - @Configuration + @Configuration(proxyBeanMethods = false) + static class CircularReferenceInSameThreadBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(TestBean testBean3) { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + + @Bean + public TestBean testBean3(TestBean testBean2) { + return new TestBean(); + } + } + + + @Configuration(proxyBeanMethods = false) + static class CircularReferenceInMultipleThreadsBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2, ObjectProvider testBean3, + ObjectProvider testBean4) { + + new Thread(testBean2::getObject).start(); + new Thread(testBean3::getObject).start(); + new Thread(testBean4::getObject).start(); + try { + Thread.sleep(3000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(ObjectProvider testBean3) { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean3.getObject()); + } + + @Bean + public TestBean testBean3(ObjectProvider testBean4) { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean4.getObject()); + } + + @Bean + public TestBean testBean4(ObjectProvider testBean2) { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean2.getObject()); + } + } + + + @Configuration(proxyBeanMethods = false) static class CustomExecutorBeanConfig { @Bean @@ -58,14 +530,14 @@ public ThreadPoolTaskExecutor bootstrapExecutor() { } @Bean(bootstrap = BACKGROUND) @DependsOn("testBean3") - public TestBean testBean1(TestBean testBean3) throws InterruptedException{ - Thread.sleep(3000); + public TestBean testBean1(TestBean testBean3) throws InterruptedException { + Thread.sleep(6000); return new TestBean(); } @Bean(bootstrap = BACKGROUND) @Lazy public TestBean testBean2() throws InterruptedException { - Thread.sleep(3000); + Thread.sleep(6000); return new TestBean(); } @@ -75,8 +547,40 @@ public TestBean testBean3() { } @Bean - public String dependent(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) { - return ""; + public TestBean testBean4(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) { + return new TestBean(); + } + } + + + @Configuration(proxyBeanMethods = false) + static class CustomExecutorLazyBeanConfig { + + @Bean + public ThreadPoolTaskExecutor bootstrapExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("Custom-"); + executor.setCorePoolSize(2); + executor.initialize(); + return executor; + } + + @Configuration(proxyBeanMethods = false) + @Lazy + static class LazyBeanConfig { + + @Bean(bootstrap = BACKGROUND) + public TestBean testBean1() throws InterruptedException { + Thread.sleep(6000); + return new TestBean(); + } + + @Bean(bootstrap = BACKGROUND) + @Lazy + public TestBean testBean2() throws InterruptedException { + Thread.sleep(6000); + return new TestBean(); + } } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanAge.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanAge.java index f37a91701f8e..1199e1ad9cb1 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BeanAge.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanAge.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanAnnotationHelperTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanAnnotationHelperTests.java new file mode 100644 index 000000000000..f6aa54ded51e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanAnnotationHelperTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.Method; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link BeanAnnotationHelper}. + * + * @author Stephane Nicoll + */ +class BeanAnnotationHelperTests { + + @BeforeEach + void clearCache() { + BeanAnnotationHelper.clearCaches(); + } + + @Test + void determineBeanNameWhenNoGeneratorAndNoBeanName() { + String beanName = BeanAnnotationHelper.determineBeanNameFor( + sampleMethod("beanWithoutName"), createBeanFactoryWithBeanNameGenerator(null)); + assertThat(beanName).isEqualTo("beanWithoutName"); + } + + @ParameterizedTest + @ValueSource(strings = { "beanWithName", "beanWithMultipleNames" }) + void determineBeanNameWhenNoGeneratorAndBeanName(String methodName) { + String beanName = BeanAnnotationHelper.determineBeanNameFor( + sampleMethod(methodName), createBeanFactoryWithBeanNameGenerator(null)); + assertThat(beanName).isEqualTo("specificName"); + } + + @Test + void determineBeanNameWhenBeanNameGeneratorAndNoBeanName() { + BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class); + String beanName = BeanAnnotationHelper.determineBeanNameFor( + sampleMethod("beanWithoutName"), createBeanFactoryWithBeanNameGenerator(beanNameGenerator)); + assertThat(beanName).isEqualTo("beanWithoutName"); + verifyNoInteractions(beanNameGenerator); + } + + @ParameterizedTest + @ValueSource(strings = { "beanWithName", "beanWithMultipleNames" }) + void determineBeanNameWhenBeanNameGeneratorAndBeanName(String methodName) { + BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class); + String beanName = BeanAnnotationHelper.determineBeanNameFor( + sampleMethod(methodName), createBeanFactoryWithBeanNameGenerator(beanNameGenerator)); + assertThat(beanName).isEqualTo("specificName"); + verifyNoInteractions(beanNameGenerator); + } + + @Test + void determineBeanNameWhenConfigurationBeanNameGeneratorAndNoBeanName() { + ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class); + when(beanNameGenerator.deriveBeanName(any(), isNull())).thenReturn("generatedBeanName"); + String beanName = BeanAnnotationHelper.determineBeanNameFor( + sampleMethod("beanWithoutName"), createBeanFactoryWithBeanNameGenerator(beanNameGenerator)); + assertThat(beanName).isEqualTo("generatedBeanName"); + verify(beanNameGenerator).deriveBeanName(any(), isNull()); + } + + @ParameterizedTest + @ValueSource(strings = { "beanWithName", "beanWithMultipleNames" }) + void determineBeanNameWhenConfigurationBeanNameGeneratorAndBeanName(String methodName) { + ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class); + given(beanNameGenerator.deriveBeanName(any(), eq("specificName"))).willReturn("generatedBeanName"); + String beanName = BeanAnnotationHelper.determineBeanNameFor( + sampleMethod(methodName), createBeanFactoryWithBeanNameGenerator(beanNameGenerator)); + assertThat(beanName).isEqualTo("generatedBeanName"); + verify(beanNameGenerator).deriveBeanName(any(), eq("specificName")); + } + + @Test + void determineBeanNameInCacheWhenNoGeneratorAndNoBeanName() { + Method method = sampleMethod("beanWithoutName"); + ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(null); + String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory); + assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName); + } + + @ParameterizedTest + @ValueSource(strings = { "beanWithName", "beanWithMultipleNames" }) + void determineBeanNameInCacheWhenNoGeneratorAndBeanName(String methodName) { + Method method = sampleMethod(methodName); + ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(null); + String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory); + assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName); + } + + @Test + void determineBeanNameInCacheWhenBeanNameGeneratorAndNoBeanName() { + BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class); + Method method = sampleMethod("beanWithoutName"); + ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(beanNameGenerator); + String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory); + assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName); + verifyNoInteractions(beanNameGenerator); + } + + @ParameterizedTest + @ValueSource(strings = { "beanWithName", "beanWithMultipleNames" }) + void determineBeanNameInCacheWhenBeanNameGeneratorAndBeanName(String methodName) { + BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class); + Method method = sampleMethod(methodName); + ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(beanNameGenerator); + String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory); + assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName); + verifyNoInteractions(beanNameGenerator); + } + + @Test + void determineBeanNameInCacheWhenConfigurationBeanNameGeneratorAndNoBeanName() { + ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class); + when(beanNameGenerator.deriveBeanName(any(), isNull())) + .thenReturn("generatedBeanName").thenReturn("generatedBeanName"); + Method method = sampleMethod("beanWithoutName"); + ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(beanNameGenerator); + String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory); + assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName); + verify(beanNameGenerator, times(2)).deriveBeanName(any(), isNull()); + } + + @ParameterizedTest + @ValueSource(strings = { "beanWithName", "beanWithMultipleNames" }) + void determineBeanNameInCacheWhenConfigurationBeanNameGeneratorAndBeanName(String methodName) { + ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class); + given(beanNameGenerator.deriveBeanName(any(), eq("specificName"))) + .willReturn("generatedBeanName").willReturn("generatedBeanName"); + Method method = sampleMethod(methodName); + ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(beanNameGenerator); + String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory); + assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName); + verify(beanNameGenerator, times(2)).deriveBeanName(any(), eq("specificName")); + } + + private static Method sampleMethod(String name) { + return Objects.requireNonNull(ReflectionUtils.findMethod(Samples.class, name)); + } + + private static ConfigurableBeanFactory createBeanFactoryWithBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) { + ConfigurableBeanFactory beanFactory = new DefaultListableBeanFactory(); + if (beanNameGenerator != null) { + beanFactory.registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator); + } + return beanFactory; + } + + + static class Samples { + + @Bean + private void beanWithoutName() {} + + @Bean(name = "specificName") + private void beanWithName() {} + + @Bean(name = { "specificName", "specificName2", "specificName3" }) + private void beanWithMultipleNames() {} + + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanLiteModeTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanLiteModeTests.java index bcc82723aa2d..63dd22d616f0 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BeanLiteModeTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanLiteModeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodMetadataTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodMetadataTests.java index 4090d428878b..81e3f166ed21 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodMetadataTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java index 1b89d020bbc0..144a3a24f214 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 Juergen Hoeller */ @SuppressWarnings("resource") -public class BeanMethodPolymorphismTests { +class BeanMethodPolymorphismTests { @Test void beanMethodDetectedOnSuperClass() { @@ -242,7 +242,8 @@ static class Config extends BaseConfig { @Configuration static class OverridingConfig extends BaseConfig { - @Bean @Lazy + @Bean + @Lazy @Override public BaseTestBean testBean() { return new BaseTestBean() { @@ -258,7 +259,8 @@ public String toString() { @Configuration static class OverridingConfigWithDifferentBeanName extends BaseConfig { - @Bean("myTestBean") @Lazy + @Bean("myTestBean") + @Lazy @Override public BaseTestBean testBean() { return new BaseTestBean() { @@ -274,7 +276,8 @@ public String toString() { @Configuration static class NarrowedOverridingConfig extends BaseConfig { - @Bean @Lazy + @Bean + @Lazy @Override public ExtendedTestBean testBean() { return new ExtendedTestBean() { @@ -287,6 +290,7 @@ public String toString() { } + @SuppressWarnings("deprecation") @Configuration(enforceUniqueMethods = false) static class ConfigWithOverloading { @@ -302,15 +306,18 @@ String aString(Integer dependency) { } + @SuppressWarnings("deprecation") @Configuration(enforceUniqueMethods = false) static class ConfigWithOverloadingAndAdditionalMetadata { - @Bean @Lazy + @Bean + @Lazy String aString() { return "regular"; } - @Bean @Lazy + @Bean + @Lazy String aString(Integer dependency) { return "overloaded" + dependency; } @@ -335,7 +342,8 @@ Integer anInt() { return 5; } - @Bean @Lazy + @Bean + @Lazy String aString(Integer dependency) { return "overloaded" + dependency; } @@ -350,7 +358,8 @@ Integer anInt() { return 5; } - @Bean @Lazy + @Bean + @Lazy String aString(List dependency) { return "overloaded" + dependency.get(0); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java index 329804f2aba0..55cbdf413bd6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,13 @@ package org.springframework.context.annotation; +import java.io.IOException; + import example.scannable.CustomComponent; import example.scannable.FooService; import example.scannable.FooServiceImpl; import example.scannable.NamedStubDao; +import example.scannable.ServiceInvocationCounter; import example.scannable.StubFooDao; import org.aspectj.lang.annotation.Aspect; import org.junit.jupiter.api.Test; @@ -35,9 +38,13 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.MessageSource; import org.springframework.context.annotation2.NamedStubDao2; +import org.springframework.context.index.CandidateComponentsIndex; +import org.springframework.context.index.CandidateComponentsIndexLoader; import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.testfixture.index.CandidateComponentsTestClassLoader; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.stereotype.Component; @@ -57,10 +64,11 @@ class ClassPathBeanDefinitionScannerTests { @Test - void testSimpleScanWithDefaultFiltersAndPostProcessors() { + void simpleScanWithDefaultFiltersAndPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isGreaterThanOrEqualTo(12); assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); assertThat(context.containsBean("fooServiceImpl")).isTrue(); @@ -73,8 +81,8 @@ void testSimpleScanWithDefaultFiltersAndPostProcessors() { assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue(); - context.refresh(); + context.refresh(); FooServiceImpl fooService = context.getBean("fooServiceImpl", FooServiceImpl.class); assertThat(context.getDefaultListableBeanFactory().containsSingleton("myNamedComponent")).isTrue(); assertThat(fooService.foo(123)).isEqualTo("bar"); @@ -83,7 +91,7 @@ void testSimpleScanWithDefaultFiltersAndPostProcessors() { } @Test - void testSimpleScanWithDefaultFiltersAndPrimaryLazyBean() { + void simpleScanWithDefaultFiltersAndPrimaryLazyBean() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.scan(BASE_PACKAGE); @@ -105,22 +113,16 @@ void testSimpleScanWithDefaultFiltersAndPrimaryLazyBean() { } @Test - void testDoubleScan() { + void simpleScanWithIndex() { GenericApplicationContext context = new GenericApplicationContext(); + context.setClassLoader(CandidateComponentsTestClassLoader.index( + ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), + new ClassPathResource("spring.components", FooServiceImpl.class))); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); - assertThat(beanCount).isGreaterThanOrEqualTo(12); - - ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(context) { - @Override - protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) { - super.postProcessBeanDefinition(beanDefinition, beanName); - beanDefinition.setAttribute("someDifference", "someValue"); - } - }; - scanner2.scan(BASE_PACKAGE); + assertThat(beanCount).isGreaterThanOrEqualTo(12); assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); assertThat(context.containsBean("fooServiceImpl")).isTrue(); assertThat(context.containsBean("stubFooDao")).isTrue(); @@ -130,16 +132,22 @@ protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, } @Test - void testWithIndex() { + void doubleScan() { GenericApplicationContext context = new GenericApplicationContext(); - context.setClassLoader(CandidateComponentsTestClassLoader.index( - ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), - new ClassPathResource("spring.components", FooServiceImpl.class))); - ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isGreaterThanOrEqualTo(12); + ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(context) { + @Override + protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) { + super.postProcessBeanDefinition(beanDefinition, beanName); + beanDefinition.setAttribute("someDifference", "someValue"); + } + }; + scanner2.scan(BASE_PACKAGE); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); assertThat(context.containsBean("fooServiceImpl")).isTrue(); assertThat(context.containsBean("stubFooDao")).isTrue(); @@ -149,7 +157,7 @@ void testWithIndex() { } @Test - void testDoubleScanWithIndex() { + void doubleScanWithIndex() { GenericApplicationContext context = new GenericApplicationContext(); context.setClassLoader(CandidateComponentsTestClassLoader.index( ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), @@ -157,6 +165,7 @@ void testDoubleScanWithIndex() { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isGreaterThanOrEqualTo(12); ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(context) { @@ -177,13 +186,13 @@ protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, } @Test - void testSimpleScanWithDefaultFiltersAndNoPostProcessors() { + void simpleScanWithDefaultFiltersAndNoPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); int beanCount = scanner.scan(BASE_PACKAGE); - assertThat(beanCount).isGreaterThanOrEqualTo(7); + assertThat(beanCount).isGreaterThanOrEqualTo(7); assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); assertThat(context.containsBean("fooServiceImpl")).isTrue(); assertThat(context.containsBean("stubFooDao")).isTrue(); @@ -192,7 +201,7 @@ void testSimpleScanWithDefaultFiltersAndNoPostProcessors() { } @Test - void testSimpleScanWithDefaultFiltersAndOverridingBean() { + void simpleScanWithDefaultFiltersAndOverridingBean() { GenericApplicationContext context = new GenericApplicationContext(); context.setAllowBeanDefinitionOverriding(true); context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class)); @@ -204,7 +213,7 @@ void testSimpleScanWithDefaultFiltersAndOverridingBean() { } @Test - void testSimpleScanWithDefaultFiltersAndOverridingBeanNotAllowed() { + void simpleScanWithDefaultFiltersAndOverridingBeanNotAllowed() { GenericApplicationContext context = new GenericApplicationContext(); context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false); context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class)); @@ -217,7 +226,7 @@ void testSimpleScanWithDefaultFiltersAndOverridingBeanNotAllowed() { } @Test - void testSimpleScanWithDefaultFiltersAndOverridingBeanAcceptedForSameBeanClass() { + void simpleScanWithDefaultFiltersAndOverridingBeanAcceptedForSameBeanClass() { GenericApplicationContext context = new GenericApplicationContext(); context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false); context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(StubFooDao.class)); @@ -229,7 +238,7 @@ void testSimpleScanWithDefaultFiltersAndOverridingBeanAcceptedForSameBeanClass() } @Test - void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() { + void simpleScanWithDefaultFiltersAndDefaultBeanNameClash() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); @@ -241,7 +250,7 @@ void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() { } @Test - void testSimpleScanWithDefaultFiltersAndOverriddenEqualNamedBean() { + void simpleScanWithDefaultFiltersAndOverriddenEqualNamedBean() { GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("myNamedDao", new RootBeanDefinition(NamedStubDao.class)); int initialBeanCount = context.getBeanDefinitionCount(); @@ -259,7 +268,7 @@ void testSimpleScanWithDefaultFiltersAndOverriddenEqualNamedBean() { } @Test - void testSimpleScanWithDefaultFiltersAndOverriddenCompatibleNamedBean() { + void simpleScanWithDefaultFiltersAndOverriddenCompatibleNamedBean() { GenericApplicationContext context = new GenericApplicationContext(); RootBeanDefinition bd = new RootBeanDefinition(NamedStubDao.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -279,7 +288,7 @@ void testSimpleScanWithDefaultFiltersAndOverriddenCompatibleNamedBean() { } @Test - void testSimpleScanWithDefaultFiltersAndSameBeanTwice() { + void simpleScanWithDefaultFiltersAndSameBeanTwice() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); @@ -289,7 +298,7 @@ void testSimpleScanWithDefaultFiltersAndSameBeanTwice() { } @Test - void testSimpleScanWithDefaultFiltersAndSpecifiedBeanNameClash() { + void simpleScanWithDefaultFiltersAndSpecifiedBeanNameClash() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); @@ -302,7 +311,7 @@ void testSimpleScanWithDefaultFiltersAndSpecifiedBeanNameClash() { } @Test - void testCustomIncludeFilterWithoutDefaultsButIncludingPostProcessors() { + void customIncludeFilterWithoutDefaultsButIncludingPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); @@ -317,7 +326,7 @@ void testCustomIncludeFilterWithoutDefaultsButIncludingPostProcessors() { } @Test - void testCustomIncludeFilterWithoutDefaultsAndNoPostProcessors() { + void customIncludeFilterWithoutDefaultsAndNoPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); @@ -337,7 +346,7 @@ void testCustomIncludeFilterWithoutDefaultsAndNoPostProcessors() { } @Test - void testCustomIncludeFilterAndDefaults() { + void customIncludeFilterAndDefaults() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); @@ -357,7 +366,7 @@ void testCustomIncludeFilterAndDefaults() { } @Test - void testCustomAnnotationExcludeFilterAndDefaults() { + void customAnnotationExcludeFilterAndDefaults() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AnnotationTypeFilter(Aspect.class)); @@ -375,7 +384,7 @@ void testCustomAnnotationExcludeFilterAndDefaults() { } @Test - void testCustomAssignableTypeExcludeFilterAndDefaults() { + void customAssignableTypeExcludeFilterAndDefaults() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); @@ -394,7 +403,7 @@ void testCustomAssignableTypeExcludeFilterAndDefaults() { } @Test - void testCustomAssignableTypeExcludeFilterAndDefaultsWithoutPostProcessors() { + void customAssignableTypeExcludeFilterAndDefaultsWithoutPostProcessors() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.setIncludeAnnotationConfig(false); @@ -412,7 +421,7 @@ void testCustomAssignableTypeExcludeFilterAndDefaultsWithoutPostProcessors() { } @Test - void testMultipleCustomExcludeFiltersAndDefaults() { + void multipleCustomExcludeFiltersAndDefaults() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); @@ -432,7 +441,7 @@ void testMultipleCustomExcludeFiltersAndDefaults() { } @Test - void testCustomBeanNameGenerator() { + void customBeanNameGenerator() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); @@ -452,7 +461,7 @@ void testCustomBeanNameGenerator() { } @Test - void testMultipleBasePackagesWithDefaultsOnly() { + void multipleBasePackagesWithDefaultsOnly() { GenericApplicationContext singlePackageContext = new GenericApplicationContext(); ClassPathBeanDefinitionScanner singlePackageScanner = new ClassPathBeanDefinitionScanner(singlePackageContext); GenericApplicationContext multiPackageContext = new GenericApplicationContext(); @@ -464,30 +473,32 @@ void testMultipleBasePackagesWithDefaultsOnly() { } @Test - void testMultipleScanCalls() { + void multipleScanCalls() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int initialBeanCount = context.getBeanDefinitionCount(); int scannedBeanCount = scanner.scan(BASE_PACKAGE); assertThat(scannedBeanCount).isGreaterThanOrEqualTo(12); - assertThat((context.getBeanDefinitionCount() - initialBeanCount)).isEqualTo(scannedBeanCount); + assertThat(context.getBeanDefinitionCount() - initialBeanCount).isEqualTo(scannedBeanCount); int addedBeanCount = scanner.scan("org.springframework.aop.aspectj.annotation"); assertThat(context.getBeanDefinitionCount()).isEqualTo((initialBeanCount + scannedBeanCount + addedBeanCount)); } @Test - void testBeanAutowiredWithAnnotationConfigEnabled() { + void beanAutowiredWithAnnotationConfigEnabled() { GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("myBf", new RootBeanDefinition(StaticListableBeanFactory.class)); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isGreaterThanOrEqualTo(12); - context.refresh(); + context.refresh(); FooServiceImpl fooService = context.getBean("fooService", FooServiceImpl.class); StaticListableBeanFactory myBf = (StaticListableBeanFactory) context.getBean("myBf"); MessageSource ms = (MessageSource) context.getBean("messageSource"); + assertThat(fooService.isInitCalled()).isTrue(); assertThat(fooService.foo(123)).isEqualTo("bar"); assertThat(fooService.lookupFoo(123)).isEqualTo("bar"); @@ -503,15 +514,16 @@ void testBeanAutowiredWithAnnotationConfigEnabled() { } @Test - void testBeanNotAutowiredWithAnnotationConfigDisabled() { + void beanNotAutowiredWithAnnotationConfigDisabled() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isGreaterThanOrEqualTo(7); - context.refresh(); + context.refresh(); try { context.getBean("fooService"); } @@ -522,7 +534,7 @@ void testBeanNotAutowiredWithAnnotationConfigDisabled() { } @Test - void testAutowireCandidatePatternMatches() { + void autowireCandidatePatternMatches() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(true); @@ -537,7 +549,7 @@ void testAutowireCandidatePatternMatches() { } @Test - void testAutowireCandidatePatternDoesNotMatch() { + void autowireCandidatePatternDoesNotMatch() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(true); @@ -545,9 +557,78 @@ void testAutowireCandidatePatternDoesNotMatch() { scanner.setAutowireCandidatePatterns("*NoSuchDao"); scanner.scan(BASE_PACKAGE); context.refresh(); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - context.getBean("fooService")) - .satisfies(ex -> assertThat(ex.getMostSpecificCause()).isInstanceOf(NoSuchBeanDefinitionException.class)); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> context.getBean("fooService")) + .satisfies(ex -> + assertThat(ex.getMostSpecificCause()).isInstanceOf(NoSuchBeanDefinitionException.class)); + } + + @Test + void withManualProgrammaticIndex() { + // Pre-populating an index in order to replace a runtime scan + + GenericApplicationContext context = new GenericApplicationContext(); + context.setResourceLoader(new RestrictedResourcePatternResolver()); + + CandidateComponentsIndex index = new CandidateComponentsIndex(); + index.registerScan("example"); + index.registerCandidateType(ServiceInvocationCounter.class.getName(), Component.class.getName()); + index.registerCandidateType(FooServiceImpl.class.getName(), Component.class.getName()); + index.registerCandidateType(StubFooDao.class.getName(), Component.class.getName()); + CandidateComponentsIndexLoader.addIndex(context.getClassLoader(), index); + + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + int beanCount = scanner.scan(BASE_PACKAGE); // from index + CandidateComponentsIndexLoader.clearCache(); + + assertThat(beanCount).isEqualTo(3); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + } + + @Test + void withDerivedProgrammaticIndex() { + // Recording an index from a scan (e.g. during refreshForAotProcessing) + + GenericApplicationContext context = new GenericApplicationContext(); + + CandidateComponentsIndex scannedIndex = new CandidateComponentsIndex(); + CandidateComponentsIndexLoader.addIndex(context.getClassLoader(), scannedIndex); + + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.scan(BASE_PACKAGE); // actual scan, populating the index instance above + CandidateComponentsIndexLoader.clearCache(); + + // Rebuilding a pre-computed index from the scanned index (AOT style) + // through String-based registerScan and registerCandidateType calls. + + context = new GenericApplicationContext(); + context.setResourceLoader(new RestrictedResourcePatternResolver()); + + CandidateComponentsIndex derivedIndex = new CandidateComponentsIndex(); + for (String basePackage : scannedIndex.getRegisteredScans()) { + derivedIndex.registerScan(basePackage); + } + for (String stereotype : scannedIndex.getRegisteredStereotypes()) { + for (String type : scannedIndex.getCandidateTypes(BASE_PACKAGE, stereotype)) { + derivedIndex.registerCandidateType(type, stereotype); + } + } + CandidateComponentsIndexLoader.addIndex(context.getClassLoader(), derivedIndex); + + scanner = new ClassPathBeanDefinitionScanner(context); + int beanCount = scanner.scan(BASE_PACKAGE); // from index + CandidateComponentsIndexLoader.clearCache(); + + assertThat(beanCount).isGreaterThanOrEqualTo(12); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean("thoreau")).isTrue(); } @@ -565,4 +646,14 @@ public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry public class NonStaticInnerClass { } + + private static final class RestrictedResourcePatternResolver extends PathMatchingResourcePatternResolver { + + @Override + public Resource[] getResources(String locationPattern) throws IOException { + throw new UnsupportedOperationException(locationPattern); + } + } + + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathFactoryBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathFactoryBeanDefinitionScannerTests.java index 0f540b8615c7..ac91c96f7d20 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathFactoryBeanDefinitionScannerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathFactoryBeanDefinitionScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class ClassPathFactoryBeanDefinitionScannerTests { @Test - void testSingletonScopedFactoryMethod() { + void singletonScopedFactoryMethod() { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); @@ -79,8 +79,7 @@ void testSingletonScopedFactoryMethod() { Object bean = context.getBean("requestScopedInstance"); //5 assertThat(AopUtils.isCglibProxy(bean)).isTrue(); - boolean condition = bean instanceof ScopedObject; - assertThat(condition).isTrue(); + assertThat(bean).isInstanceOf(ScopedObject.class); QualifiedClientBean clientBean = context.getBean("clientBean", QualifiedClientBean.class); assertThat(clientBean.testBean).isSameAs(context.getBean("publicInstance")); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java index f7880f4910dc..c6ef88e0a8ab 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,7 @@ import java.util.stream.Stream; import example.gh24375.AnnotatedComponent; -import example.indexed.IndexedJakartaManagedBeanComponent; import example.indexed.IndexedJakartaNamedComponent; -import example.indexed.IndexedJavaxManagedBeanComponent; -import example.indexed.IndexedJavaxNamedComponent; import example.profilescan.DevComponent; import example.profilescan.ProfileAnnotatedComponent; import example.profilescan.ProfileMetaAnnotatedComponent; @@ -40,13 +37,11 @@ import example.scannable.FooDao; import example.scannable.FooService; import example.scannable.FooServiceImpl; -import example.scannable.JakartaManagedBeanComponent; import example.scannable.JakartaNamedComponent; -import example.scannable.JavaxManagedBeanComponent; -import example.scannable.JavaxNamedComponent; import example.scannable.MessageBean; import example.scannable.NamedComponent; import example.scannable.NamedStubDao; +import example.scannable.OtherFooService; import example.scannable.ScopedProxyTestBean; import example.scannable.ServiceInvocationCounter; import example.scannable.StubFooDao; @@ -91,30 +86,13 @@ class ClassPathScanningCandidateComponentProviderTests { private static final Set> springComponents = Set.of( DefaultNamedComponent.class, - NamedComponent.class, FooServiceImpl.class, - StubFooDao.class, + NamedComponent.class, NamedStubDao.class, + OtherFooService.class, ServiceInvocationCounter.class, - BarComponent.class - ); - - private static final Set> scannedJakartaComponents = Set.of( - JakartaNamedComponent.class, - JakartaManagedBeanComponent.class - ); - - private static final Set> scannedJavaxComponents = Set.of( - JavaxNamedComponent.class, - JavaxManagedBeanComponent.class - ); - - private static final Set> indexedComponents = Set.of( - IndexedJakartaNamedComponent.class, - IndexedJakartaManagedBeanComponent.class, - IndexedJavaxNamedComponent.class, - IndexedJavaxManagedBeanComponent.class - ); + StubFooDao.class, + BarComponent.class); @Test @@ -122,28 +100,25 @@ void defaultsWithScan() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); provider.setResourceLoader(new DefaultResourceLoader( CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); - testDefault(provider, TEST_BASE_PACKAGE, true, true, false); + testDefault(provider, TEST_BASE_PACKAGE, true, false); } @Test void defaultsWithIndex() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); - testDefault(provider, "example", true, true, true); + testDefault(provider, "example", true, true); } private void testDefault(ClassPathScanningCandidateComponentProvider provider, String basePackage, - boolean includeScannedJakartaComponents, boolean includeScannedJavaxComponents, boolean includeIndexedComponents) { + boolean includeScannedJakartaComponents, boolean includeIndexedComponents) { Set> expectedTypes = new HashSet<>(springComponents); if (includeScannedJakartaComponents) { - expectedTypes.addAll(scannedJakartaComponents); - } - if (includeScannedJavaxComponents) { - expectedTypes.addAll(scannedJavaxComponents); + expectedTypes.add(JakartaNamedComponent.class); } if (includeIndexedComponents) { - expectedTypes.addAll(indexedComponents); + expectedTypes.add(IndexedJakartaNamedComponent.class); } Set candidates = provider.findCandidateComponents(basePackage); @@ -216,7 +191,7 @@ void customAnnotationTypeIncludeFilterWithIndex() { private void testCustomAnnotationTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider) { provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); - testDefault(provider, TEST_BASE_PACKAGE, false, false, false); + testDefault(provider, TEST_BASE_PACKAGE, false, false); } @Test @@ -239,7 +214,8 @@ private void testCustomAssignableTypeIncludeFilter(ClassPathScanningCandidateCom Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); assertScannedBeanDefinitions(candidates); // Interfaces/Abstract class are filtered out automatically. - assertBeanTypes(candidates, AutowiredQualifierFooService.class, FooServiceImpl.class, ScopedProxyTestBean.class); + assertBeanTypes(candidates, + AutowiredQualifierFooService.class, FooServiceImpl.class, OtherFooService.class, ScopedProxyTestBean.class); } @Test @@ -263,7 +239,8 @@ private void testCustomSupportedIncludeAndExcludeFilter(ClassPathScanningCandida provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); assertScannedBeanDefinitions(candidates); - assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); + assertBeanTypes(candidates, + NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); } @Test @@ -308,8 +285,9 @@ void excludeFilterWithIndex() { private void testExclude(ClassPathScanningCandidateComponentProvider provider) { Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); assertScannedBeanDefinitions(candidates); - assertBeanTypes(candidates, FooServiceImpl.class, StubFooDao.class, ServiceInvocationCounter.class, - BarComponent.class, JakartaManagedBeanComponent.class, JavaxManagedBeanComponent.class); + assertBeanTypes(candidates, + FooServiceImpl.class, OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class, + BarComponent.class); } @Test @@ -327,7 +305,8 @@ void withComponentAnnotationOnly() { provider.addExcludeFilter(new AnnotationTypeFilter(Service.class)); provider.addExcludeFilter(new AnnotationTypeFilter(Controller.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); - assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); + assertBeanTypes(candidates, + NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); } @Test @@ -360,8 +339,9 @@ void withMultipleMatchingFilters() { provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); - assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, FooServiceImpl.class, - BarComponent.class, DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class); + assertBeanTypes(candidates, + DefaultNamedComponent.class, FooServiceImpl.class, NamedComponent.class, NamedStubDao.class, + OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class); } @Test @@ -371,8 +351,9 @@ void excludeTakesPrecedence() { provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); provider.addExcludeFilter(new AssignableTypeFilter(FooService.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); - assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class, - DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class); + assertBeanTypes(candidates, + DefaultNamedComponent.class, NamedComponent.class, NamedStubDao.class, + ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java index b08c67573eb2..6a22e1679dd8 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,16 +116,6 @@ void postConstructAndPreDestroyWithApplicationContextAndPostProcessor() { assertThat(bean.destroyCalled).isTrue(); } - @Test - void postConstructAndPreDestroyWithLegacyAnnotations() { - bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LegacyAnnotatedInitDestroyBean.class)); - - LegacyAnnotatedInitDestroyBean bean = (LegacyAnnotatedInitDestroyBean) bf.getBean("annotatedBean"); - assertThat(bean.initCalled).isTrue(); - bf.destroySingletons(); - assertThat(bean.destroyCalled).isTrue(); - } - @Test void postConstructAndPreDestroyWithManualConfiguration() { InitDestroyAnnotationBeanPostProcessor bpp = new InitDestroyAnnotationBeanPostProcessor(); @@ -223,26 +213,6 @@ void resourceInjectionWithPrototypes() { assertThat(bean.destroy3Called).isTrue(); } - @Test - void resourceInjectionWithLegacyAnnotations() { - bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LegacyResourceInjectionBean.class)); - TestBean tb = new TestBean(); - bf.registerSingleton("testBean", tb); - TestBean tb2 = new TestBean(); - bf.registerSingleton("testBean2", tb2); - - LegacyResourceInjectionBean bean = (LegacyResourceInjectionBean) bf.getBean("annotatedBean"); - assertThat(bean.initCalled).isTrue(); - assertThat(bean.init2Called).isTrue(); - assertThat(bean.init3Called).isTrue(); - assertThat(bean.getTestBean()).isSameAs(tb); - assertThat(bean.getTestBean2()).isSameAs(tb2); - bf.destroySingletons(); - assertThat(bean.destroyCalled).isTrue(); - assertThat(bean.destroy2Called).isTrue(); - assertThat(bean.destroy3Called).isTrue(); - } - @Test void resourceInjectionWithResolvableDependencyType() { bpp.setBeanFactory(bf); @@ -257,7 +227,7 @@ void resourceInjectionWithResolvableDependencyType() { bf.registerResolvableDependency(BeanFactory.class, bf); bf.registerResolvableDependency(INestedTestBean.class, (ObjectFactory) NestedTestBean::new); - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); Properties props = new Properties(); props.setProperty("tb", "testBean4"); @@ -342,7 +312,7 @@ void extendedResourceInjection() { bf.addBeanPostProcessor(bpp); bf.registerResolvableDependency(BeanFactory.class, bf); - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); Properties props = new Properties(); props.setProperty("tb", "testBean3"); @@ -393,7 +363,7 @@ void extendedResourceInjectionWithOverriding() { bf.addBeanPostProcessor(bpp); bf.registerResolvableDependency(BeanFactory.class, bf); - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); Properties props = new Properties(); props.setProperty("tb", "testBean3"); @@ -431,8 +401,7 @@ void extendedResourceInjectionWithOverriding() { bf.getBean("annotatedBean2"); } catch (BeanCreationException ex) { - boolean condition = ex.getRootCause() instanceof NoSuchBeanDefinitionException; - assertThat(condition).isTrue(); + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); NoSuchBeanDefinitionException innerEx = (NoSuchBeanDefinitionException) ex.getRootCause(); assertThat(innerEx.getBeanName()).isEqualTo("testBean9"); } @@ -558,30 +527,6 @@ private void destroy() { } - public static class LegacyAnnotatedInitDestroyBean { - - public boolean initCalled = false; - - public boolean destroyCalled = false; - - @javax.annotation.PostConstruct - private void init() { - if (this.initCalled) { - throw new IllegalStateException("Already called"); - } - this.initCalled = true; - } - - @javax.annotation.PreDestroy - private void destroy() { - if (this.destroyCalled) { - throw new IllegalStateException("Already called"); - } - this.destroyCalled = true; - } - } - - public static class InitDestroyBeanPostProcessor implements DestructionAwareBeanPostProcessor { @Override @@ -691,83 +636,6 @@ public TestBean getTestBean2() { } - public static class LegacyResourceInjectionBean extends LegacyAnnotatedInitDestroyBean { - - public boolean init2Called = false; - - public boolean init3Called = false; - - public boolean destroy2Called = false; - - public boolean destroy3Called = false; - - @javax.annotation.Resource - private TestBean testBean; - - private TestBean testBean2; - - @javax.annotation.PostConstruct - protected void init2() { - if (this.testBean == null || this.testBean2 == null) { - throw new IllegalStateException("Resources not injected"); - } - if (!this.initCalled) { - throw new IllegalStateException("Superclass init method not called yet"); - } - if (this.init2Called) { - throw new IllegalStateException("Already called"); - } - this.init2Called = true; - } - - @javax.annotation.PostConstruct - private void init() { - if (this.init3Called) { - throw new IllegalStateException("Already called"); - } - this.init3Called = true; - } - - @javax.annotation.PreDestroy - protected void destroy2() { - if (this.destroyCalled) { - throw new IllegalStateException("Superclass destroy called too soon"); - } - if (this.destroy2Called) { - throw new IllegalStateException("Already called"); - } - this.destroy2Called = true; - } - - @javax.annotation.PreDestroy - private void destroy() { - if (this.destroyCalled) { - throw new IllegalStateException("Superclass destroy called too soon"); - } - if (this.destroy3Called) { - throw new IllegalStateException("Already called"); - } - this.destroy3Called = true; - } - - @javax.annotation.Resource - public void setTestBean2(TestBean testBean2) { - if (this.testBean2 != null) { - throw new IllegalStateException("Already called"); - } - this.testBean2 = testBean2; - } - - public TestBean getTestBean() { - return testBean; - } - - public TestBean getTestBean2() { - return testBean2; - } - } - - static class NonPublicResourceInjectionBean extends ResourceInjectionBean { @Resource(name="testBean4", type=TestBean.class) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java index 7f4e591bc4f4..ffa94bc7a631 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,7 +79,7 @@ void contributeWhenPrivateFieldInjectionInjectsUsingReflection() { RegisteredBean registeredBean = getAndApplyContribution( PrivateFieldResourceSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onField(PrivateFieldResourceSample.class, "one")) + .onType(PrivateFieldResourceSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PrivateFieldResourceSample instance = new PrivateFieldResourceSample(); @@ -92,13 +92,13 @@ void contributeWhenPrivateFieldInjectionInjectsUsingReflection() { @Test @CompileWithForkedClassLoader - void contributeWhenPackagePrivateFieldInjectionInjectsUsingFieldAssignement() { + void contributeWhenPackagePrivateFieldInjectionInjectsUsingFieldAssignment() { this.beanFactory.registerSingleton("one", "1"); this.beanFactory.registerSingleton("two", "2"); RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateFieldResourceSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onField(PackagePrivateFieldResourceSample.class, "one")) + .onType(PackagePrivateFieldResourceSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateFieldResourceSample instance = new PackagePrivateFieldResourceSample(); @@ -117,7 +117,7 @@ void contributeWhenPackagePrivateFieldInjectionOnParentClassInjectsUsingReflecti RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateFieldResourceFromParentSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onField(PackagePrivateFieldResourceSample.class, "one")) + .onType(PackagePrivateFieldResourceSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateFieldResourceFromParentSample instance = new PackagePrivateFieldResourceFromParentSample(); @@ -135,7 +135,7 @@ void contributeWhenPrivateMethodInjectionInjectsUsingReflection() { RegisteredBean registeredBean = getAndApplyContribution( PrivateMethodResourceSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PrivateMethodResourceSample.class, "setOne").invoke()) + .onMethodInvocation(PrivateMethodResourceSample.class, "setOne")) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PrivateMethodResourceSample instance = new PrivateMethodResourceSample(); @@ -153,7 +153,7 @@ void contributeWhenPrivateMethodInjectionWithCustomNameInjectsUsingReflection() RegisteredBean registeredBean = getAndApplyContribution( PrivateMethodResourceWithCustomNameSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PrivateMethodResourceWithCustomNameSample.class, "setText").invoke()) + .onMethodInvocation(PrivateMethodResourceWithCustomNameSample.class, "setText")) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PrivateMethodResourceWithCustomNameSample instance = new PrivateMethodResourceWithCustomNameSample(); @@ -172,7 +172,7 @@ void contributeWhenPackagePrivateMethodInjectionInjectsUsingMethodInvocation() { RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateMethodResourceSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PackagePrivateMethodResourceSample.class, "setOne").introspect()) + .onType(PackagePrivateMethodResourceSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateMethodResourceSample instance = new PackagePrivateMethodResourceSample(); @@ -191,7 +191,7 @@ void contributeWhenPackagePrivateMethodInjectionOnParentClassInjectsUsingReflect RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateMethodResourceFromParentSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PackagePrivateMethodResourceSample.class, "setOne")) + .onMethodInvocation(PackagePrivateMethodResourceSample.class, "setOne")) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateMethodResourceFromParentSample instance = new PackagePrivateMethodResourceFromParentSample(); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAndImportAnnotationInteractionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAndImportAnnotationInteractionTests.java index 35ca617f1ede..b735f5e536f6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAndImportAnnotationInteractionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAndImportAnnotationInteractionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 Chris Beams * @since 3.1 */ -public class ComponentScanAndImportAnnotationInteractionTests { +class ComponentScanAndImportAnnotationInteractionTests { @Test void componentScanOverlapsWithImport() { @@ -101,10 +101,4 @@ static final class Config2 { static final class Config3 { } - - @ComponentScan("org.springframework.context.annotation.componentscan.simple") - @ComponentScan("org.springframework.context.annotation.componentscan.importing") - public static final class ImportedConfig { - } - } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java index 0603f4d6193c..e8f189fdcf47 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 example.scannable.CustomStereotype; import example.scannable.DefaultNamedComponent; import example.scannable.FooService; +import example.scannable.FooServiceImpl; import example.scannable.MessageBean; import example.scannable.ScopedProxyTestBean; import example.scannable_implicitbasepackage.ComponentScanAnnotatedConfigWithImplicitBasePackage; @@ -43,6 +44,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; @@ -84,6 +86,17 @@ void controlScan() { assertContextContainsBean(ctx, "fooServiceImpl"); } + @Test + void controlScanWithExplicitRegistration() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("myFooService", new RootBeanDefinition(FooServiceImpl.class)); + ctx.scan(example.scannable.PackageMarker.class.getPackage().getName()); + ctx.refresh(); + + assertContextContainsBean(ctx, "myFooService"); + assertContextContainsBean(ctx, "fooServiceImpl"); + } + @Test void viaContextRegistration() { ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanAnnotatedConfig.class); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationRecursionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationRecursionTests.java index e72605f9f120..36b351c7095e 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationRecursionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationRecursionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java index 458608eed41b..b9f5e1d89413 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserBeanDefinitionDefaultsTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserBeanDefinitionDefaultsTests.java index 84deb54af237..108f19913351 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserBeanDefinitionDefaultsTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserBeanDefinitionDefaultsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ void setUp() { } @Test - void testDefaultLazyInit() { + void defaultLazyInit() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); @@ -54,7 +54,7 @@ void testDefaultLazyInit() { } @Test - void testLazyInitTrue() { + void lazyInitTrue() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultLazyInitTrueTests.xml"); @@ -67,7 +67,7 @@ void testLazyInitTrue() { } @Test - void testLazyInitFalse() { + void lazyInitFalse() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultLazyInitFalseTests.xml"); @@ -78,7 +78,7 @@ void testLazyInitFalse() { } @Test - void testDefaultAutowire() { + void defaultAutowire() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); @@ -90,7 +90,7 @@ void testDefaultAutowire() { } @Test - void testAutowireNo() { + void autowireNo() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireNoTests.xml"); @@ -102,7 +102,7 @@ void testAutowireNo() { } @Test - void testAutowireConstructor() { + void autowireConstructor() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireConstructorTests.xml"); @@ -115,7 +115,7 @@ void testAutowireConstructor() { } @Test - void testAutowireByType() { + void autowireByType() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireByTypeTests.xml"); @@ -124,7 +124,7 @@ void testAutowireByType() { } @Test - void testAutowireByName() { + void autowireByName() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireByNameTests.xml"); @@ -137,7 +137,7 @@ void testAutowireByName() { } @Test - void testDefaultDependencyCheck() { + void defaultDependencyCheck() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); @@ -149,7 +149,7 @@ void testDefaultDependencyCheck() { } @Test - void testDefaultInitAndDestroyMethodsNotDefined() { + void defaultInitAndDestroyMethodsNotDefined() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); @@ -161,7 +161,7 @@ void testDefaultInitAndDestroyMethodsNotDefined() { } @Test - void testDefaultInitAndDestroyMethodsDefined() { + void defaultInitAndDestroyMethodsDefined() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultInitAndDestroyMethodsTests.xml"); @@ -173,7 +173,7 @@ void testDefaultInitAndDestroyMethodsDefined() { } @Test - void testDefaultNonExistingInitAndDestroyMethodsDefined() { + void defaultNonExistingInitAndDestroyMethodsDefined() { GenericApplicationContext context = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultNonExistingInitAndDestroyMethodsTests.xml"); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserScopedProxyTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserScopedProxyTests.java index 39603286200c..bd3a1b5d98ea 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserScopedProxyTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserScopedProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class ComponentScanParserScopedProxyTests { @Test - void testDefaultScopedProxy() { + void defaultScopedProxy() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/scopedProxyDefaultTests.xml"); context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); @@ -49,7 +49,7 @@ void testDefaultScopedProxy() { } @Test - void testNoScopedProxy() { + void noScopedProxy() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/scopedProxyNoTests.xml"); context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); @@ -61,7 +61,7 @@ void testNoScopedProxy() { } @Test - void testInterfacesScopedProxy() throws Exception { + void interfacesScopedProxy() throws Exception { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/scopedProxyInterfacesTests.xml"); context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); @@ -79,7 +79,7 @@ void testInterfacesScopedProxy() throws Exception { } @Test - void testTargetClassScopedProxy() throws Exception { + void targetClassScopedProxy() throws Exception { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/scopedProxyTargetClassTests.xml"); context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); @@ -97,7 +97,7 @@ void testTargetClassScopedProxy() throws Exception { @Test @SuppressWarnings("resource") - public void testInvalidConfigScopedProxy() { + void invalidConfigScopedProxy() { assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> new ClassPathXmlApplicationContext("org/springframework/context/annotation/scopedProxyInvalidConfigTests.xml")) .withMessageContaining("Cannot define both 'scope-resolver' and 'scoped-proxy' on tag") diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java index 22e9fd690b52..85b7a8c91659 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserWithUserDefinedStrategiesTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserWithUserDefinedStrategiesTests.java index ae0ac9bc3bda..60d7ec203a37 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserWithUserDefinedStrategiesTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserWithUserDefinedStrategiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java index 0cc872791822..a22a4e984aed 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBeanMethodTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBeanMethodTests.java index 91876dbdf6a3..3438c50a3bdb 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBeanMethodTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBeanMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -128,7 +128,7 @@ void verifyToString() throws Exception { .startsWith("ConfigurationClass: beanName 'Config1', class path resource"); List beanMethods = getBeanMethods(configurationClass); - String prefix = "BeanMethod: " + Config1.class.getName(); + String prefix = "BeanMethod: java.lang.String " + Config1.class.getName(); assertThat(beanMethods.get(0).toString()).isEqualTo(prefix + ".bean0()"); assertThat(beanMethods.get(1).toString()).isEqualTo(prefix + ".bean1(java.lang.String)"); assertThat(beanMethods.get(2).toString()).isEqualTo(prefix + ".bean2(java.lang.String,java.lang.Integer)"); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java new file mode 100644 index 000000000000..fedeaba9b045 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -0,0 +1,262 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.ProtectionDomain; +import java.security.SecureClassLoader; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.core.OverridingClassLoader; +import org.springframework.core.SmartClassLoader; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Phillip Webb + * @author Juergen Hoeller + */ +class ConfigurationClassEnhancerTests { + + @Test + void enhanceReloadedClass() throws Exception { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader parentClassLoader = getClass().getClassLoader(); + ClassLoader classLoader = new CustomSmartClassLoader(parentClassLoader); + Class myClass = parentClassLoader.loadClass(MyConfig.class.getName()); + Class enhancedClass = configurationClassEnhancer.enhance(myClass, parentClassLoader); + assertThat(myClass).isAssignableFrom(enhancedClass); + + myClass = classLoader.loadClass(MyConfig.class.getName()); + enhancedClass = configurationClassEnhancer.enhance(myClass, classLoader); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + assertThat(myClass).isAssignableFrom(enhancedClass); + } + + @Test + void withPublicClass() { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); + Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); + assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + + classLoader = new OverridingClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); + assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new CustomSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); + assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); + assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + } + + @Test + void withNonPublicClass() { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); + Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader); + assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new OverridingClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader); + assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new CustomSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader); + assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader); + assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + } + + @Test + void withNonPublicConstructor() { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); + Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new OverridingClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new CustomSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + } + + @Test + void withNonPublicMethod() { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); + Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); + assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new OverridingClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); + assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new CustomSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); + assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); + assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + } + + + @Configuration + static class MyConfig { + + @Bean + String myBean() { + return "bean"; + } + } + + + @Configuration + public static class MyConfigWithPublicClass { + + @Bean + public String myBean() { + return "bean"; + } + } + + + @Configuration + static class MyConfigWithNonPublicClass { + + @Bean + public String myBean() { + return "bean"; + } + } + + + @Configuration + public static class MyConfigWithNonPublicConstructor { + + MyConfigWithNonPublicConstructor() { + } + + @Bean + public String myBean() { + return "bean"; + } + } + + + @Configuration + public static class MyConfigWithNonPublicMethod { + + @Bean + String myBean() { + return "bean"; + } + } + + + static class CustomSmartClassLoader extends SecureClassLoader implements SmartClassLoader { + + CustomSmartClassLoader(ClassLoader parent) { + super(parent); + } + + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.contains("MyConfig")) { + String path = name.replace('.', '/').concat(".class"); + try (InputStream in = super.getResourceAsStream(path)) { + byte[] bytes = StreamUtils.copyToByteArray(in); + if (bytes.length > 0) { + return defineClass(name, bytes, 0, bytes.length); + } + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + return super.loadClass(name, resolve); + } + + @Override + public boolean isClassReloadable(Class clazz) { + return clazz.getName().contains("MyConfig"); + } + + @Override + public ClassLoader getOriginalClassLoader() { + return getParent(); + } + + @Override + public Class publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) { + return defineClass(name, b, 0, b.length, protectionDomain); + } + } + + + static class BasicSmartClassLoader extends SecureClassLoader implements SmartClassLoader { + + BasicSmartClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + public Class publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) { + return defineClass(name, b, 0, b.length, protectionDomain); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostConstructAndAutowiringTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostConstructAndAutowiringTests.java index 8c91db3cd5fd..3eb5ff5be8b1 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostConstructAndAutowiringTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostConstructAndAutowiringTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java index 9b7777cbf9f9..656e6143f4ad 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.context.annotation; +import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; import java.util.function.BiConsumer; @@ -24,7 +25,9 @@ import javax.lang.model.element.Modifier; +import jakarta.annotation.PostConstruct; import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -36,7 +39,10 @@ import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -53,15 +59,16 @@ import org.springframework.context.testfixture.context.generator.SimpleComponent; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.DefaultPropertySourceFactory; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; import org.springframework.core.test.tools.Compiled; import org.springframework.core.test.tools.TestCompiler; import org.springframework.core.type.AnnotationMetadata; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; @@ -74,6 +81,7 @@ * @author Phillip Webb * @author Stephane Nicoll * @author Sam Brannen + * @author Sebastien Deleuze */ class ConfigurationClassPostProcessorAotContributionTests { @@ -168,6 +176,24 @@ void applyToWhenHasImportAwareConfigurationRegistersHints() { )); } + @Test + void applyToWhenHasImportAwareBeanRegistrarRegistersHints() { + BeanFactoryInitializationAotContribution contribution = getContribution(BeanRegistrarTests.ImportAwareConfiguration.class); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + assertThat(generationContext.getRuntimeHints().resources().resourcePatternHints()) + .singleElement() + .satisfies(resourceHint -> assertThat(resourceHint.getIncludes()) + .map(ResourcePatternHint::getPattern) + .containsExactlyInAnyOrder( + "/", + "org", + "org/springframework", + "org/springframework/context", + "org/springframework/context/annotation", + "org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests$BeanRegistrarTests$ImportAwareConfiguration.class" + )); + } + @SuppressWarnings("unchecked") private void compile(BiConsumer, Compiled> result) { MethodReference methodReference = beanFactoryInitializationCode.getInitializers().get(0); @@ -223,9 +249,8 @@ public void setImportMetadata(AnnotationMetadata importMetadata) { this.metadata = importMetadata; } - @Nullable @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + public @Nullable Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (beanName.equals("testProcessing")) { return this.metadata; } @@ -440,9 +465,246 @@ private RegisteredBean getRegisteredBean(Class bean) { } } + @Nested + class BeanRegistrarTests { + + @Test + void applyToWhenHasDefaultConstructor() throws NoSuchMethodException { + BeanFactoryInitializationAotContribution contribution = getContribution(DefaultConstructorConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + Constructor fooConstructor = Foo.class.getDeclaredConstructor(); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(Foo.class)).isNotNull(); + assertThat(RuntimeHintsPredicates.reflection().onConstructorInvocation(fooConstructor)) + .accepts(generationContext.getRuntimeHints()); + freshContext.close(); + }); + } + + @Test + void applyToWhenHasInstanceSupplier() { + BeanFactoryInitializationAotContribution contribution = getContribution(InstanceSupplierConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(Foo.class)).isNotNull(); + assertThat(generationContext.getRuntimeHints().reflection().getTypeHint(Foo.class)).isNull(); + freshContext.close(); + }); + } + + @Test + void applyToWhenHasPostConstructAnnotationPostProcessed() { + BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class, + PostConstructConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + Init init = freshContext.getBean(Init.class); + assertThat(init).isNotNull(); + assertThat(init.initialized).isTrue(); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(Init.class, "postConstruct")) + .accepts(generationContext.getRuntimeHints()); + freshContext.close(); + }); + } + + @Test + void applyToWhenIsImportAware() { + BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class, + ImportAwareConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(ClassNameHolder.class).className()) + .isEqualTo(ImportAwareConfiguration.class.getName()); + freshContext.close(); + }); + } + + @Test + @CompileWithForkedClassLoader + void applyToWhenIsPackagePrivate() throws NoSuchMethodException { + BeanFactoryInitializationAotContribution contribution = getContribution(PackagePrivateConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + Constructor fooConstructor = Foo.class.getDeclaredConstructor(); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(Foo.class)).isNotNull(); + assertThat(RuntimeHintsPredicates.reflection().onConstructorInvocation(fooConstructor)) + .accepts(generationContext.getRuntimeHints()); + freshContext.close(); + }); + } + + @Test + @CompileWithForkedClassLoader + void applyToWhenIsPackagePrivateAndImportAware() { + BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class, + PackagePrivateAndImportAwareConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(ClassNameHolder.class).className()) + .isEqualTo(PackagePrivateAndImportAwareConfiguration.class.getName()); + freshContext.close(); + }); + } + + @SuppressWarnings("unchecked") + private void compile(BiConsumer, Compiled> result) { + MethodReference methodReference = beanFactoryInitializationCode.getInitializers().get(0); + beanFactoryInitializationCode.getTypeBuilder().set(type -> { + ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator + .of(ListableBeanFactory.class, "applicationContext.getBeanFactory()") + .and(ArgumentCodeGenerator.of(Environment.class, "applicationContext.getEnvironment()")); + CodeBlock methodInvocation = methodReference.toInvokeCodeBlock(argCodeGenerator, + beanFactoryInitializationCode.getClassName()); + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get(Consumer.class, GenericApplicationContext.class)); + type.addMethod(MethodSpec.methodBuilder("accept").addModifiers(Modifier.PUBLIC) + .addParameter(GenericApplicationContext.class, "applicationContext") + .addStatement(methodInvocation) + .build()); + }); + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(generationContext).compile(compiled -> + result.accept(compiled.getInstance(Consumer.class), compiled)); + } + + + @Configuration + @Import(DefaultConstructorBeanRegistrar.class) + public static class DefaultConstructorConfiguration { + } + + public static class DefaultConstructorBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Foo.class); + } + } + + @Configuration + @Import(InstanceSupplierBeanRegistrar.class) + public static class InstanceSupplierConfiguration { + } + + public static class InstanceSupplierBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Foo.class, spec -> spec.supplier(context -> new Foo())); + } + } + + @Configuration + @Import(PostConstructBeanRegistrar.class) + public static class PostConstructConfiguration { + } + + public static class PostConstructBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Init.class); + } + } + + @Import(ImportAwareBeanRegistrar.class) + public static class ImportAwareConfiguration { + } + + public static class ImportAwareBeanRegistrar implements BeanRegistrar, ImportAware { + + @Nullable + private AnnotationMetadata importMetadata; + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(ClassNameHolder.class, spec -> spec.supplier(context -> + new ClassNameHolder(this.importMetadata == null ? null : this.importMetadata.getClassName()))); + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + } + + @Configuration + @Import(PackagePrivateBeanRegistrar.class) + static class PackagePrivateConfiguration { + } + + static class PackagePrivateBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Foo.class); + } + } + + @Import(PackagePrivateAndImportAwareBeanRegistrar.class) + static class PackagePrivateAndImportAwareConfiguration { + } + + static class PackagePrivateAndImportAwareBeanRegistrar implements BeanRegistrar, ImportAware { + + @Nullable + private AnnotationMetadata importMetadata; + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(ClassNameHolder.class, spec -> spec.supplier(context -> + new ClassNameHolder(this.importMetadata == null ? null : this.importMetadata.getClassName()))); + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + } + + static class Foo { + } + + static class Init { + + boolean initialized = false; + + @PostConstruct + void postConstruct() { + initialized = true; + } + } + + } + + public record ClassNameHolder(@Nullable String className) {} + - @Nullable - private BeanFactoryInitializationAotContribution getContribution(Class... types) { + private @Nullable BeanFactoryInitializationAotContribution getContribution(Class... types) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); for (Class type : types) { beanFactory.registerBeanDefinition(type.getName(), new RootBeanDefinition(type)); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java index ef810f56a5f5..422cc1fd7a3a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 jakarta.annotation.PostConstruct; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.SimpleTraceInterceptor; @@ -50,6 +52,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 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.ChildBeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -65,17 +68,28 @@ import org.springframework.core.io.DescriptiveResource; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.core.type.MethodMetadata; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** + * Tests for {@link ConfigurationClassPostProcessor}. + * * @author Chris Beams * @author Juergen Hoeller * @author Sam Brannen + * @author Stephane Nicoll */ class ConfigurationClassPostProcessorTests { @@ -104,6 +118,7 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespected() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR); Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isSameAs(foo); @@ -118,6 +133,7 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR); Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isSameAs(foo); @@ -126,12 +142,29 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() { assertThat(beanFactory.getDependentBeans("config")).contains("bar"); } + @Test // gh-34663 + void enhancementIsPresentForAbstractConfigClassWithoutBeanMethods() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(AbstractConfigWithoutBeanMethods.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + RootBeanDefinition beanDefinition = (RootBeanDefinition) beanFactory.getBeanDefinition("config"); + assertThat(beanDefinition.hasBeanClass()).isTrue(); + assertThat(beanDefinition.getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + assertThat(beanFactory.getDependentBeans("foo")).contains("bar"); + String[] dependentsOfSingletonBeanConfig = beanFactory.getDependentBeans(SingletonBeanConfig.class.getName()); + assertThat(dependentsOfSingletonBeanConfig).containsOnly("foo", "bar"); + } + @Test void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isNotSameAs(foo); @@ -143,6 +176,7 @@ void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalseUsingAsm() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isNotSameAs(foo); @@ -154,6 +188,7 @@ void enhancementIsNotPresentForStaticMethods() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("foo")).hasBeanClass()).isTrue(); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("bar")).hasBeanClass()).isTrue(); Foo foo = beanFactory.getBean("foo", Foo.class); @@ -167,6 +202,7 @@ void enhancementIsNotPresentForStaticMethodsUsingAsm() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("foo")).hasBeanClass()).isTrue(); assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("bar")).hasBeanClass()).isTrue(); Foo foo = beanFactory.getBean("foo", Foo.class); @@ -174,6 +210,15 @@ void enhancementIsNotPresentForStaticMethodsUsingAsm() { assertThat(bar.foo).isNotSameAs(foo); } + @Test // gh-34486 + void enhancementIsNotPresentWithEmptyConfig() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(EmptyConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR); + } + @Test void configurationIntrospectionOfInnerClassesWorksWithDotNameSyntax() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(getClass().getName() + ".SingletonBeanConfig")); @@ -377,11 +422,14 @@ void postProcessorFailsOnImplicitOverrideIfOverridingIsNotAllowed() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); beanFactory.setAllowBeanDefinitionOverriding(false); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + assertThatExceptionOfType(BeanDefinitionStoreException.class) .isThrownBy(() -> pp.postProcessBeanFactory(beanFactory)) - .withMessageContaining("bar") - .withMessageContaining("SingletonBeanConfig") - .withMessageContaining(TestBean.class.getName()); + .withMessageContainingAll( + "bar", + "SingletonBeanConfig", + TestBean.class.getName() + ); } @Test // gh-25430 @@ -390,10 +438,13 @@ void detectAliasOverride() { DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(); beanFactory.setAllowBeanDefinitionOverriding(false); context.register(FirstConfiguration.class, SecondConfiguration.class); + assertThatIllegalStateException().isThrownBy(context::refresh) - .withMessageContaining("alias 'taskExecutor'") - .withMessageContaining("name 'applicationTaskExecutor'") - .withMessageContaining("bean definition 'taskExecutor'"); + .withMessageContainingAll( + "alias 'taskExecutor'", + "name 'applicationTaskExecutor'", + "bean definition 'taskExecutor'" + ); context.close(); } @@ -406,8 +457,7 @@ void configurationClassesProcessedInCorrectOrder() { pp.postProcessBeanFactory(beanFactory); Foo foo = beanFactory.getBean(Foo.class); - boolean condition = foo instanceof ExtendedFoo; - assertThat(condition).isTrue(); + assertThat(foo).isInstanceOf(ExtendedFoo.class); Bar bar = beanFactory.getBean(Bar.class); assertThat(bar.foo).isSameAs(foo); } @@ -422,8 +472,7 @@ void configurationClassesWithValidOverridingForProgrammaticCall() { pp.postProcessBeanFactory(beanFactory); Foo foo = beanFactory.getBean(Foo.class); - boolean condition = foo instanceof ExtendedAgainFoo; - assertThat(condition).isTrue(); + assertThat(foo).isInstanceOf(ExtendedAgainFoo.class); Bar bar = beanFactory.getBean(Bar.class); assertThat(bar.foo).isSameAs(foo); } @@ -453,8 +502,7 @@ void nestedConfigurationClassesProcessedInCorrectOrder() { pp.postProcessBeanFactory(beanFactory); Foo foo = beanFactory.getBean(Foo.class); - boolean condition = foo instanceof ExtendedFoo; - assertThat(condition).isTrue(); + assertThat(foo).isInstanceOf(ExtendedFoo.class); Bar bar = beanFactory.getBean(Bar.class); assertThat(bar.foo).isSameAs(foo); } @@ -468,8 +516,7 @@ void innerConfigurationClassesProcessedInCorrectOrder() { beanFactory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor()); Foo foo = beanFactory.getBean(Foo.class); - boolean condition = foo instanceof ExtendedFoo; - assertThat(condition).isTrue(); + assertThat(foo).isInstanceOf(ExtendedFoo.class); Bar bar = beanFactory.getBean(Bar.class); assertThat(bar.foo).isSameAs(foo); } @@ -485,8 +532,7 @@ void scopedProxyTargetMarkedAsNonAutowireCandidate() { pp.postProcessBeanFactory(beanFactory); ITestBean injected = beanFactory.getBean("consumer", ScopedProxyConsumer.class).testBean; - boolean condition = injected instanceof ScopedObject; - assertThat(condition).isTrue(); + assertThat(injected).isInstanceOf(ScopedObject.class); assertThat(injected).isSameAs(beanFactory.getBean("scopedClass")); assertThat(injected).isSameAs(beanFactory.getBean(ITestBean.class)); } @@ -504,6 +550,67 @@ void processingAllowedOnlyOncePerProcessorRegistryPair() { pp.postProcessBeanFactory(bf2)); // second invocation for bf2 -- should throw } + @Test + void beanDefinitionsFromBeanMethodWithoutBeanNameGenerator() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(BeanNamesConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(beanFactory.getBeanDefinitionNames()) + .containsOnly("config", "beanWithoutName", "specificName", "specificNames"); + assertThat(beanFactory.getBean("beanWithoutName")).isEqualTo("beanWithoutName"); + assertThat(beanFactory.getBean("specificName")).isEqualTo("beanWithName"); + assertThat(beanFactory.getBean("specificNames")).isEqualTo("beanWithMultipleNames"); + assertThat(beanFactory.getBean("specificNames2")).isEqualTo("beanWithMultipleNames"); + assertThat(beanFactory.getBean("specificNames3")).isEqualTo("beanWithMultipleNames"); + } + + @Test + void beanDefinitionsFromBeanMethodWithBeanNameGenerator() { + BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(BeanNamesConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.setBeanNameGenerator(beanNameGenerator); + pp.postProcessBeanFactory(beanFactory); + assertThat(beanFactory.getBeanDefinitionNames()) + .containsOnly("config", "beanWithoutName", "specificName", "specificNames"); + assertThat(beanFactory.getBean("beanWithoutName")).isEqualTo("beanWithoutName"); + assertThat(beanFactory.getBean("specificName")).isEqualTo("beanWithName"); + assertThat(beanFactory.getBean("specificNames")).isEqualTo("beanWithMultipleNames"); + assertThat(beanFactory.getBean("specificNames2")).isEqualTo("beanWithMultipleNames"); + assertThat(beanFactory.getBean("specificNames3")).isEqualTo("beanWithMultipleNames"); + verifyNoInteractions(beanNameGenerator); + } + + @Test + void beanDefinitionsFromBeanMethodWithConfigurationBeanNameGenerator() { + ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class); + Answer answer = invocation -> { + MethodMetadata methodMetadata = invocation.getArgument(0); + String providedBeanName = invocation.getArgument(1); + return (providedBeanName != null) ? "test.fromBean." + providedBeanName : "test." + methodMetadata.getMethodName(); + }; + given(beanNameGenerator.deriveBeanName(any(), any())).willAnswer(answer).willAnswer(answer).willAnswer(answer); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(BeanNamesConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.setBeanNameGenerator(beanNameGenerator); + pp.postProcessBeanFactory(beanFactory); + assertThat(beanFactory.getBeanDefinitionNames()) + .containsOnly("config", "test.beanWithoutName", "test.fromBean.specificName", "test.fromBean.specificNames"); + assertThat(beanFactory.getBean("test.beanWithoutName")).isEqualTo("beanWithoutName"); + assertThat(beanFactory.getBean("test.fromBean.specificName")).isEqualTo("beanWithName"); + assertThat(beanFactory.getBean("test.fromBean.specificNames")).isEqualTo("beanWithMultipleNames"); + assertThat(beanFactory.getBean("specificNames2")).isEqualTo("beanWithMultipleNames"); + assertThat(beanFactory.getBean("specificNames3")).isEqualTo("beanWithMultipleNames"); + ArgumentCaptor methodMetadataCaptor = ArgumentCaptor.forClass(MethodMetadata.class); + ArgumentCaptor beanNameCaptor = ArgumentCaptor.forClass(String.class); + verify(beanNameGenerator, times(3)).deriveBeanName(methodMetadataCaptor.capture(), beanNameCaptor.capture()); + List beansMethodMetadata = methodMetadataCaptor.getAllValues(); + assertThat(beansMethodMetadata).map(MethodMetadata::getMethodName) + .containsExactly("beanWithoutName", "beanWithName", "beanWithMultipleNames"); + List beanNames = beanNameCaptor.getAllValues(); + assertThat(beanNames).containsExactly(null, "specificName", "specificNames"); + } + @Test void genericsBasedInjection() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); @@ -946,7 +1053,7 @@ void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawInstance() { } @Test - void testSelfReferenceExclusionForFactoryMethodOnSameBean() { + void selfReferenceExclusionForFactoryMethodOnSameBean() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -960,7 +1067,7 @@ void testSelfReferenceExclusionForFactoryMethodOnSameBean() { } @Test - void testConfigWithDefaultMethods() { + void configWithDefaultMethods() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -974,7 +1081,7 @@ void testConfigWithDefaultMethods() { } @Test - void testConfigWithDefaultMethodsUsingAsm() { + void configWithDefaultMethodsUsingAsm() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -988,7 +1095,7 @@ void testConfigWithDefaultMethodsUsingAsm() { } @Test - void testConfigWithFailingInit() { // gh-23343 + void configWithFailingInit() { // gh-23343 AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -1002,7 +1109,7 @@ void testConfigWithFailingInit() { // gh-23343 } @Test - void testCircularDependency() { + void circularDependency() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -1016,42 +1123,42 @@ void testCircularDependency() { } @Test - void testCircularDependencyWithApplicationContext() { + void circularDependencyWithApplicationContext() { assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(() -> new AnnotationConfigApplicationContext(A.class, AStrich.class)) .withMessageContaining("Circular reference"); } @Test - void testPrototypeArgumentThroughBeanMethodCall() { + void prototypeArgumentThroughBeanMethodCall() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithPrototype.class); ctx.getBean(FooFactory.class).createFoo(new BarArgument()); ctx.close(); } @Test - void testSingletonArgumentThroughBeanMethodCall() { + void singletonArgumentThroughBeanMethodCall() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithSingleton.class); ctx.getBean(FooFactory.class).createFoo(new BarArgument()); ctx.close(); } @Test - void testNullArgumentThroughBeanMethodCall() { + void nullArgumentThroughBeanMethodCall() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithNull.class); ctx.getBean("aFoo"); ctx.close(); } @Test - void testInjectionPointMatchForNarrowTargetReturnType() { + void injectionPointMatchForNarrowTargetReturnType() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(FooBarConfiguration.class); assertThat(ctx.getBean(FooImpl.class).bar).isSameAs(ctx.getBean(BarImpl.class)); ctx.close(); } @Test - void testVarargOnBeanMethod() { + void varargOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class, TestBean.class); VarargConfiguration bean = ctx.getBean(VarargConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1061,7 +1168,7 @@ void testVarargOnBeanMethod() { } @Test - void testEmptyVarargOnBeanMethod() { + void emptyVarargOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class); VarargConfiguration bean = ctx.getBean(VarargConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1070,7 +1177,7 @@ void testEmptyVarargOnBeanMethod() { } @Test - void testCollectionArgumentOnBeanMethod() { + void collectionArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class, TestBean.class); CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class)); @@ -1078,7 +1185,7 @@ void testCollectionArgumentOnBeanMethod() { } @Test - void testEmptyCollectionArgumentOnBeanMethod() { + void emptyCollectionArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class); CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); assertThat(bean.testBeans).isEmpty(); @@ -1086,7 +1193,7 @@ void testEmptyCollectionArgumentOnBeanMethod() { } @Test - void testMapArgumentOnBeanMethod() { + void mapArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class, DummyRunnable.class); MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); assertThat(bean.testBeans).hasSize(1).containsValue(ctx.getBean(Runnable.class)); @@ -1094,7 +1201,7 @@ void testMapArgumentOnBeanMethod() { } @Test - void testEmptyMapArgumentOnBeanMethod() { + void emptyMapArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class); MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); assertThat(bean.testBeans).isEmpty(); @@ -1102,7 +1209,7 @@ void testEmptyMapArgumentOnBeanMethod() { } @Test - void testCollectionInjectionFromSameConfigurationClass() { + void collectionInjectionFromSameConfigurationClass() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionInjectionConfiguration.class); CollectionInjectionConfiguration bean = ctx.getBean(CollectionInjectionConfiguration.class); assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class)); @@ -1110,7 +1217,7 @@ void testCollectionInjectionFromSameConfigurationClass() { } @Test - void testMapInjectionFromSameConfigurationClass() { + void mapInjectionFromSameConfigurationClass() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapInjectionConfiguration.class); MapInjectionConfiguration bean = ctx.getBean(MapInjectionConfiguration.class); assertThat(bean.testBeans).containsOnly(Map.entry("testBean", ctx.getBean(Runnable.class))); @@ -1118,20 +1225,20 @@ void testMapInjectionFromSameConfigurationClass() { } @Test - void testBeanLookupFromSameConfigurationClass() { + void beanLookupFromSameConfigurationClass() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanLookupConfiguration.class); assertThat(ctx.getBean(BeanLookupConfiguration.class).getTestBean()).isSameAs(ctx.getBean(TestBean.class)); ctx.close(); } @Test - void testNameClashBetweenConfigurationClassAndBean() { + void nameClashBetweenConfigurationClassAndBean() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class).getBean("myTestBean", TestBean.class)); + .isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class)); } @Test - void testBeanDefinitionRegistryPostProcessorConfig() { + void beanDefinitionRegistryPostProcessorConfig() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanDefinitionRegistryPostProcessorConfig.class); assertThat(ctx.getBean("myTestBean")).isInstanceOf(TestBean.class); ctx.close(); @@ -1166,7 +1273,7 @@ static class NonEnhancedSingletonBeanConfig { } @Configuration - static class StaticSingletonBeanConfig { + static final class StaticSingletonBeanConfig { @Bean public static Foo foo() { @@ -1179,6 +1286,16 @@ public static Bar bar() { } } + @Configuration + @Import(SingletonBeanConfig.class) + abstract static class AbstractConfigWithoutBeanMethods { + // This class intentionally does NOT declare @Bean methods. + } + + @Configuration + static final class EmptyConfig { + } + @Configuration @Order(2) static class OverridingSingletonBeanConfig { @@ -1342,6 +1459,26 @@ public ITestBean scopedClass() { } } + @Configuration(proxyBeanMethods = false) + public static class BeanNamesConfig { + + @Bean + public String beanWithoutName() { + return "beanWithoutName"; + } + + @Bean(name = "specificName") + public String beanWithName() { + return "beanWithName"; + } + + @Bean(name = { "specificNames", "specificNames2", "specificNames3" }) + public String beanWithMultipleNames() { + return "beanWithMultipleNames"; + } + + } + public interface RepositoryInterface { @Override diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java index ee89a8fa4331..fdf0e8be2ce2 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 Juergen Hoeller */ @SuppressWarnings("resource") -public class ConfigurationClassWithConditionTests { +class ConfigurationClassWithConditionTests { @Test void conditionalOnMissingBeanMatch() { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndAutowiringTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndAutowiringTests.java index 5176eed7fba4..615acaa1e5c4 100755 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndAutowiringTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndAutowiringTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndParametersTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndParametersTests.java index e4bdc9972ce2..6863fd4cb4db 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndParametersTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndParametersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java index 28a803f60ad2..c37657eedebb 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolverTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolverTests.java new file mode 100644 index 000000000000..dec1da48e28e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolverTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.core.MethodParameter; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ContextAnnotationAutowireCandidateResolver}. + * + * @author Sam Brannen + * @since 7.0.4 + */ +class ContextAnnotationAutowireCandidateResolverTests { + + final ContextAnnotationAutowireCandidateResolver resolver = new ContextAnnotationAutowireCandidateResolver(); + + Method testMethod; + + @RegisterExtension + BeforeTestExecutionCallback extension = context -> this.testMethod = context.getRequiredTestMethod(); + + + @Test + void isNotLazy() { + assertNotLazy(); + } + + @Test + void isLazy() { + assertLazy(); + } + + @Test + void isMetaLazy() { + assertLazy(); + } + + @Test // gh-36306 + void isMetaMetaLazy() { + assertLazy(); + } + + private void assertLazy() { + assertThat(this.resolver.isLazy(getMethodDescriptor())) + .as("%sMethod() is @Lazy", this.testMethod.getName()).isTrue(); + assertThat(this.resolver.isLazy(getParameterDescriptor())) + .as("parameter in %sParameter() is @Lazy", this.testMethod.getName()).isTrue(); + } + + private void assertNotLazy() { + assertThat(this.resolver.isLazy(getMethodDescriptor())) + .as("%sMethod() is not @Lazy", this.testMethod.getName()).isFalse(); + assertThat(this.resolver.isLazy(getParameterDescriptor())) + .as("parameter in %sParameter() is not @Lazy", this.testMethod.getName()).isFalse(); + } + + private DependencyDescriptor getMethodDescriptor() { + var method = ReflectionUtils.findMethod(getClass(), this.testMethod.getName() + "Method"); + var methodParameter = MethodParameter.forExecutable(method, -1); + return new DependencyDescriptor(methodParameter, true); + } + + private DependencyDescriptor getParameterDescriptor() { + var method = ReflectionUtils.findMethod(getClass(), this.testMethod.getName() + "Parameter", String.class); + var methodParameter = MethodParameter.forExecutable(method, 0); + return new DependencyDescriptor(methodParameter, true); + } + + + void isNotLazyMethod() { + } + + @Lazy + void isLazyMethod() { + } + + @MetaLazy + void isMetaLazyMethod() { + } + + @MetaMetaLazy + void isMetaMetaLazyMethod() { + } + + void isNotLazyParameter(String enigma) { + } + + void isLazyParameter(@Lazy String enigma) { + } + + void isMetaLazyParameter(@MetaLazy String enigma) { + } + + void isMetaMetaLazyParameter(@MetaMetaLazy String enigma) { + } + + + @Lazy + @Retention(RetentionPolicy.RUNTIME) + @interface MetaLazy { + } + + @MetaLazy + @Retention(RetentionPolicy.RUNTIME) + @interface MetaMetaLazy { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java index 9efc276b4963..d3bcbdac747f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/DestroyMethodInferenceTests.java b/spring-context/src/test/java/org/springframework/context/annotation/DestroyMethodInferenceTests.java index 57e19faf9239..4b44a1b7b1e0 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/DestroyMethodInferenceTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/DestroyMethodInferenceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/DoubleScanTests.java b/spring-context/src/test/java/org/springframework/context/annotation/DoubleScanTests.java index c540d08116c5..445908f67c02 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/DoubleScanTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/DoubleScanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java b/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java index 953bb98e1740..1f758bfff609 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ void withJdkProxy() { aspectIsApplied(ctx); assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean(FooService.class))).isTrue(); + assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue(); ctx.close(); } @@ -56,6 +57,7 @@ void withCglibProxy() { aspectIsApplied(ctx); assertThat(AopUtils.isCglibProxy(ctx.getBean(FooService.class))).isTrue(); + assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue(); ctx.close(); } @@ -124,7 +126,7 @@ static class ConfigWithCglibProxy { } - @Import({ ServiceInvocationCounter.class, StubFooDao.class }) + @Import({ServiceInvocationCounter.class, StubFooDao.class}) @EnableAspectJAutoProxy(exposeProxy = true) static class ConfigWithExposedProxy { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java index 31d7efa6f514..b1052cd05cfc 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/FactoryMethodResolutionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/FactoryMethodResolutionTests.java index 8c4c67accecd..62eb3d46f654 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/FactoryMethodResolutionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/FactoryMethodResolutionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/FooServiceDependentConverter.java b/spring-context/src/test/java/org/springframework/context/annotation/FooServiceDependentConverter.java index 34af301685ba..ffa3e346979a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/FooServiceDependentConverter.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/FooServiceDependentConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Gh23206Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Gh23206Tests.java index 08f4bbbf7641..9913bb0c9ecc 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Gh23206Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Gh23206Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.context.ApplicationContextException; +import org.springframework.context.annotation.Gh23206Tests.ConditionalConfiguration.NestedConfiguration; import org.springframework.context.annotation.componentscan.simple.SimpleComponent; import org.springframework.core.type.AnnotatedTypeMetadata; @@ -30,7 +31,7 @@ * * @author Stephane Nicoll */ -public class Gh23206Tests { +class Gh23206Tests { @Test void componentScanShouldFailWithRegisterBeanCondition() { @@ -39,7 +40,9 @@ void componentScanShouldFailWithRegisterBeanCondition() { assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(context::refresh) .withMessageContaining(ConditionalComponentScanConfiguration.class.getName()) .havingCause().isInstanceOf(ApplicationContextException.class) - .withMessageContaining("Component scan could not be used with conditions in REGISTER_BEAN phase"); + .withMessageStartingWith("Component scan for configuration class [") + .withMessageContaining(ConditionalComponentScanConfiguration.class.getName()) + .withMessageContaining("could not be used with conditions in REGISTER_BEAN phase"); } @Test @@ -49,7 +52,9 @@ void componentScanShouldFailWithRegisterBeanConditionOnClasThatImportedIt() { assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(context::refresh) .withMessageContaining(ConditionalConfiguration.class.getName()) .havingCause().isInstanceOf(ApplicationContextException.class) - .withMessageContaining("Component scan could not be used with conditions in REGISTER_BEAN phase"); + .withMessageStartingWith("Component scan for configuration class [") + .withMessageContaining(NestedConfiguration.class.getName()) + .withMessageContaining("could not be used with conditions in REGISTER_BEAN phase"); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Gh29105Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Gh29105Tests.java index d8faf0f1587f..099392a11525 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Gh29105Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Gh29105Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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,9 @@ void beanProviderWithParentContextReuseOrder() { Stream> orderedTypes = child.getBeanProvider(MyService.class).orderedStream().map(Object::getClass); assertThat(orderedTypes).containsExactly(CustomService.class, DefaultService.class); + assertThat(child.getDefaultListableBeanFactory().getOrder("defaultService")).isEqualTo(0); + assertThat(child.getDefaultListableBeanFactory().getOrder("customService")).isEqualTo(-1); + child.close(); parent.close(); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Gh32489Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Gh32489Tests.java index ea91c188ecd9..4da34eb07ae3 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Gh32489Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Gh32489Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 Gh32489Tests { +class Gh32489Tests { @Test void resolveFactoryBeansWithWildcard() { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareAotBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareAotBeanPostProcessorTests.java index 85b0181f06ac..186032eeab68 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareAotBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareAotBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareTests.java index 146f261a1a03..c8a523838540 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrarTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrarTests.java index 725bd5d7e25f..526727fa1c23 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrarTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java index e04987b3bfe8..619642c951f8 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -46,7 +47,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -64,7 +64,7 @@ * @author Stephane Nicoll */ @SuppressWarnings("resource") -public class ImportSelectorTests { +class ImportSelectorTests { static Map, String> importFrom = new HashMap<>(); @@ -91,6 +91,17 @@ void importSelectors() { ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any()); } + @Test + void filteredImportSelector() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(FilteredConfig.class); + context.refresh(); + String[] beanNames = context.getBeanFactory().getBeanDefinitionNames(); + assertThat(beanNames).endsWith("importSelectorTests.FilteredConfig", + ImportedSelector2.class.getName(), "b"); + assertThat(beanNames).doesNotContain("a", Object.class.getName(), "c"); + } + @Test void invokeAwareMethodsInImportSelector() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AwareConfig.class); @@ -274,6 +285,25 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { } } + @Configuration + @Import(FilteredImportSelector.class) + public static class FilteredConfig { + } + + public static class FilteredImportSelector implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { ImportedSelector1.class.getName(), ImportedSelector2.class.getName(), ImportedSelector3.class.getName() }; + } + + @Override + public Predicate getExclusionFilter() { + return (className -> className.equals(ImportedSelector1.class.getName()) || + className.equals(ImportedSelector3.class.getName())); + } + } + public static class DeferredImportSelector1 implements DeferredImportSelector, Ordered { @@ -320,6 +350,15 @@ public String b() { } } + @Configuration + public static class ImportedSelector3 { + + @Bean + public String c() { + return "c"; + } + } + @Configuration public static class DeferredImportedSelector1 { @@ -364,8 +403,7 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { } @Override - @Nullable - public Predicate getExclusionFilter() { + public @Nullable Predicate getExclusionFilter() { return className -> className.endsWith("ImportedSelector1"); } } @@ -401,18 +439,16 @@ static class GroupedConfig2 { public static class GroupedDeferredImportSelector1 extends DeferredImportSelector1 { - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } } public static class GroupedDeferredImportSelector2 extends DeferredImportSelector2 { - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } } @@ -432,9 +468,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] { DeferredImportSelector1.class.getName(), ChildConfiguration1.class.getName() }; } - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } @@ -453,9 +488,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] { DeferredImportSelector2.class.getName(), ChildConfiguration2.class.getName() }; } - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } @@ -476,9 +510,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] { DeferredImportedSelector3.class.getName() }; } - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } @@ -498,9 +531,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] { DeferredImportSelector2.class.getName() }; } - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportVersusDirectRegistrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportVersusDirectRegistrationTests.java index 9325ec7412c6..0f4564ae6968 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ImportVersusDirectRegistrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportVersusDirectRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportedConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportedConfig.java new file mode 100644 index 000000000000..15fd4a6a8d9b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportedConfig.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +@ComponentScan("org.springframework.context.annotation.componentscan.simple") +@ComponentScan("org.springframework.context.annotation.componentscan.importing") +public final class ImportedConfig { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java b/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java index fe78bd41de00..d7bf6c42abb5 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/InitDestroyMethodLifecycleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java index bd2bb4a41022..0b3ea564ccd6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ /** * Unit tests covering cases where a user defines an invalid Configuration - * class, e.g.: forgets to annotate with {@link Configuration} or declares + * class, for example: forgets to annotate with {@link Configuration} or declares * a Configuration class as final. * * @author Chris Beams @@ -37,16 +37,18 @@ class InvalidConfigurationClassDefinitionTests { @Test void configurationClassesMayNotBeFinal() { @Configuration - final class Config { } + final class Config { + @Bean String dummy() { return "dummy"; } + } BeanDefinition configBeanDef = rootBeanDefinition(Config.class).getBeanDefinition(); DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition("config", configBeanDef); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> - pp.postProcessBeanFactory(beanFactory)) - .withMessageContaining("Remove the final modifier"); + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> pp.postProcessBeanFactory(beanFactory)) + .withMessageContaining("Remove the final modifier"); } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java index 0eecab38364d..365233fdb72e 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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 org.junit.jupiter.api.Test; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.Advised; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; @@ -67,7 +69,7 @@ private void doTestLazyResourceInjection(Class annotat } @Test - void lazyResourceInjectionWithField() { + void lazyResourceInjectionWithField() throws Exception { doTestLazyResourceInjection(FieldResourceInjectionBean.class); AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); @@ -84,9 +86,36 @@ void lazyResourceInjectionWithField() { assertThat(bean.getTestBeans()).isNotEmpty(); assertThat(bean.getTestBeans().get(0).getName()).isNull(); assertThat(ac.getBeanFactory().containsSingleton("testBean")).isTrue(); + TestBean tb = (TestBean) ac.getBean("testBean"); tb.setName("tb"); assertThat(bean.getTestBean().getName()).isSameAs("tb"); + + assertThat(bean.getTestBeans()).isInstanceOf(Advised.class); + TargetSource targetSource = ((Advised) bean.getTestBeans()).getTargetSource(); + assertThat(targetSource.getTarget()).isSameAs(targetSource.getTarget()); + + ac.close(); + } + + @Test + void lazyResourceInjectionWithFieldForPrototype() { + doTestLazyResourceInjection(FieldResourceInjectionBean.class); + + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); + RootBeanDefinition abd = new RootBeanDefinition(FieldResourceInjectionBean.class); + abd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + ac.registerBeanDefinition("annotatedBean", abd); + RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class); + tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + tbd.setLazyInit(true); + ac.registerBeanDefinition("testBean", tbd); + ac.refresh(); + + FieldResourceInjectionBean bean = ac.getBean("annotatedBean", FieldResourceInjectionBean.class); + assertThat(bean.getTestBeans()).isNotEmpty(); + TestBean tb = bean.getTestBeans().get(0); + assertThat(bean.getTestBeans().get(0)).isNotSameAs(tb); ac.close(); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/MyTestBean.java b/spring-context/src/test/java/org/springframework/context/annotation/MyTestBean.java index 5baa5c6040e0..b5edbc0501e4 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/MyTestBean.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/MyTestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java b/spring-context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java index f961a3577150..4ec886bb5458 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java index 2a1910220847..a9d5a2cba7e6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PrimitiveBeanLookupAndAutowiringTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PrimitiveBeanLookupAndAutowiringTests.java index 3c40987c11ca..66f32183f6ca 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/PrimitiveBeanLookupAndAutowiringTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/PrimitiveBeanLookupAndAutowiringTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java index df6a0723be32..de7d02a4bdf6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ReflectionUtilsIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ReflectionUtilsIntegrationTests.java index 6a31d3bbc228..d10613fc4a20 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ReflectionUtilsIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ReflectionUtilsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverFieldTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverFieldTests.java index 35d65eeefccb..71056becf714 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverFieldTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverFieldTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverMethodTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverMethodTests.java index 555f082c2776..897525f99647 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverMethodTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ResourceElementResolverMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -134,20 +134,16 @@ static class TestBean { private String one; - private String test; - - private Integer count; - public void setOne(String one) { this.one = one; } public void setTest(String test) { - this.test = test; + // no-op } public void setCount(Integer count) { - this.count = count; + // no-op } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/RoleAndDescriptionAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/RoleAndDescriptionAnnotationTests.java index f145c4af2c96..caeb70049e78 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/RoleAndDescriptionAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/RoleAndDescriptionAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/SimpleConfigTests.java b/spring-context/src/test/java/org/springframework/context/annotation/SimpleConfigTests.java index 8331d931c7d7..a905ba04fbcb 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/SimpleConfigTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/SimpleConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class SimpleConfigTests { @Test - void testFooService() throws Exception { + void fooService() throws Exception { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(getConfigLocations(), getClass()); FooService fooService = ctx.getBean("fooServiceImpl", FooService.class); @@ -44,8 +44,7 @@ void testFooService() throws Exception { assertThat(value).isEqualTo("bar"); Future future = fooService.asyncFoo(1); - boolean condition = future instanceof FutureTask; - assertThat(condition).isTrue(); + assertThat(future).isInstanceOf(FutureTask.class); assertThat(future.get()).isEqualTo("bar"); assertThat(serviceInvocationCounter.getCount()).isEqualTo(2); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/SimpleScanTests.java b/spring-context/src/test/java/org/springframework/context/annotation/SimpleScanTests.java index 846a5e5e7f83..e6889b3e2525 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/SimpleScanTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/SimpleScanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ protected String[] getConfigLocations() { } @Test - void testFooService() { + void fooService() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(getConfigLocations(), getClass()); FooService fooService = (FooService) ctx.getBean("fooServiceImpl"); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr11202Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr11202Tests.java index 48cf378bf7ad..62064b8bbc15 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr11202Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr11202Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr11310Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr11310Tests.java index b5a3fb2624c5..6cb37391c6d9 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr11310Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr11310Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr12278Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr12278Tests.java index f74a5fc02468..93332f10f6a0 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr12278Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr12278Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr12636Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr12636Tests.java index 19d2ae6e3e35..5b4125f5df25 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr12636Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr12636Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr15042Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr15042Tests.java index f947331dbc5d..a81c06dd4d28 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr15042Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr15042Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr15275Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr15275Tests.java index b7041709b142..085ebb2d4eb2 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr15275Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr15275Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java index eb9ce351622d..5cd23998154f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ void repro() { assertThat(bf.getBean(AssemblerInjection.class).assembler4).isSameAs(bf.getBean("pageAssembler")); assertThat(bf.getBean(AssemblerInjection.class).assembler5).isSameAs(bf.getBean("pageAssembler")); assertThat(bf.getBean(AssemblerInjection.class).assembler6).isSameAs(bf.getBean("pageAssembler")); + assertThat(bf.getBean(AssemblerInjection.class).assembler7).isSameAs(bf.getBean("pageAssembler")); } } @@ -80,6 +81,9 @@ public static class AssemblerInjection { @Autowired(required = false) PageAssembler assembler6; + + @Autowired(required = false) + PageAssembler assembler7; } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java index 0bf17159e29e..02d1bea02ab6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class Spr16217Tests { @Test - public void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInRegisterBeanPhase() { + void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInRegisterBeanPhase() { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RegisterBeanPhaseImportingConfiguration.class)) { context.getBean("someBean"); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr6602Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr6602Tests.java index 93cf350fca70..4ab41cb1764c 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr6602Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr6602Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,12 +34,12 @@ class Spr6602Tests { @Test - void testXmlBehavior() throws Exception { + void xmlBehavior() throws Exception { doAssertions(new ClassPathXmlApplicationContext("Spr6602Tests-context.xml", Spr6602Tests.class)); } @Test - void testConfigurationClassBehavior() throws Exception { + void configurationClassBehavior() throws Exception { doAssertions(new AnnotationConfigApplicationContext(FooConfig.class)); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr8954Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr8954Tests.java index bc6205794c00..8716277cfb64 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr8954Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr8954Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ * @author Oliver Gierke */ @SuppressWarnings("resource") -public class Spr8954Tests { +class Spr8954Tests { @Test void repro() { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/TestBeanNameGenerator.java b/spring-context/src/test/java/org/springframework/context/annotation/TestBeanNameGenerator.java index 7a42ce63162f..47b14fb81f8f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/TestBeanNameGenerator.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/TestBeanNameGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/TestScopeMetadataResolver.java b/spring-context/src/test/java/org/springframework/context/annotation/TestScopeMetadataResolver.java index 8a8bec8478fd..f112d14a998d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/TestScopeMetadataResolver.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/TestScopeMetadataResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java new file mode 100644 index 000000000000..c0ffebbe88fe --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation.beanregistrar; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.testfixture.beans.factory.BarRegistrar; +import org.springframework.context.testfixture.beans.factory.ConditionalBeanRegistrar; +import org.springframework.context.testfixture.beans.factory.FooRegistrar; +import org.springframework.context.testfixture.beans.factory.GenericBeanRegistrar; +import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Bar; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Baz; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Foo; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Init; +import org.springframework.context.testfixture.context.annotation.registrar.BeanRegistrarConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.ComponentBeanRegistrar; +import org.springframework.context.testfixture.context.annotation.registrar.ComponentBeanRegistrar.IgnoredFromComponent; +import org.springframework.context.testfixture.context.annotation.registrar.ConditionalBeanRegistrarConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar; +import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.BeanBeanRegistrar; +import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.IgnoredFromBean; +import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.IgnoredFromConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.GenericBeanRegistrarConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.ImportAwareBeanRegistrarConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.MultipleBeanRegistrarsConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.TestBeanConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link BeanRegistrar} imported by @{@link org.springframework.context.annotation.Configuration}. + * + * @author Sebastien Deleuze + * @author Stephane Nicoll + */ +class BeanRegistrarConfigurationTests { + + @Test + void beanRegistrar() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanRegistrarConfiguration.class); + assertThat(context.getBean(Bar.class).foo()).isEqualTo(context.getBean(Foo.class)); + assertThat(context.getBean("foo", Foo.class)).isEqualTo(context.getBean("fooAlias", Foo.class)); + assertThatThrownBy(() -> context.getBean(Baz.class)).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(context.getBean(Init.class).initialized).isTrue(); + BeanDefinition beanDefinition = context.getBeanDefinition("bar"); + assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE); + assertThat(beanDefinition.isLazyInit()).isTrue(); + assertThat(beanDefinition.getDescription()).isEqualTo("Custom description"); + } + + @Test + void beanRegistrarIgnoreBeans() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigurationBeanRegistrar.class); + assertThatNoException().isThrownBy(() -> context.getBean(ConfigurationBeanRegistrar.class)); + assertThatNoException().isThrownBy(() -> context.getBean(BeanBeanRegistrar.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromConfiguration.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromBean.class)); + } + + @Test + void beanRegistrarWithClasspathScanningIgnoreBeans() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.scan("org.springframework.context.testfixture.context.annotation.registrar"); + context.refresh(); + + assertThatNoException().isThrownBy(() -> context.getBean(ConfigurationBeanRegistrar.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromConfiguration.class)); + + assertThatNoException().isThrownBy(() -> context.getBean(BeanBeanRegistrar.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromBean.class)); + + assertThatNoException().isThrownBy(() -> context.getBean(ComponentBeanRegistrar.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromComponent.class)); + } + + @Test + void beanRegistrarWithProfile() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(BeanRegistrarConfiguration.class); + context.getEnvironment().addActiveProfile("baz"); + context.refresh(); + assertThat(context.getBean(Baz.class).message()).isEqualTo("Hello World!"); + } + + @Test + void scannedFunctionalConfiguration() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.scan("org.springframework.context.testfixture.context.annotation.registrar"); + context.refresh(); + assertThat(context.getBean(Bar.class).foo()).isEqualTo(context.getBean(Foo.class)); + assertThatThrownBy(() -> context.getBean(Baz.class).message()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(context.getBean(Init.class).initialized).isTrue(); + } + + @Test + void beanRegistrarWithTargetType() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(GenericBeanRegistrarConfiguration.class); + context.refresh(); + RootBeanDefinition beanDefinition = (RootBeanDefinition)context.getBeanDefinition("fooSupplier"); + assertThat(beanDefinition.getResolvableType().resolveGeneric(0)).isEqualTo(GenericBeanRegistrar.Foo.class); + } + + @Test + void beanRegistrarWithImportAware() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ImportAwareBeanRegistrarConfiguration.class); + context.refresh(); + assertThat(context.getBean(ImportAwareBeanRegistrar.ClassNameHolder.class).className()) + .isEqualTo(ImportAwareBeanRegistrarConfiguration.class.getName()); + } + + @Test + void multipleBeanRegistrars() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(MultipleBeanRegistrarsConfiguration.class); + context.refresh(); + assertThat(context.getBean(FooRegistrar.Foo.class)).isNotNull(); + assertThat(context.getBean(BarRegistrar.Bar.class)).isNotNull(); + } + + @Test + void programmaticBeanRegistrarIsInvokedBeforeConfigurationClassPostProcessor() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(TestBeanConfiguration.class); + context.register(new ConditionalBeanRegistrar()); + context.refresh(); + assertThat(context.containsBean("myTestBean")).isFalse(); + } + + @Test + void programmaticBeanRegistrarHandlesProgrammaticRegisteredBean() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(new ConditionalBeanRegistrar()); + context.registerBean("testBean", TestBean.class); + context.refresh(); + assertThat(context.containsBean("myTestBean")).isTrue(); + assertThat(context.getBean("myTestBean")).isInstanceOf(TestBean.class); + } + + @Test + void importedBeanRegistrarWithConditionNotMet() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ConditionalBeanRegistrarConfiguration.class); + context.register(TestBeanConfiguration.class); + context.refresh(); + assertThat(context.containsBean("myTestBean")).isFalse(); + } + + @Test + void importedBeanRegistrarWithConditionMet() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(TestBeanConfiguration.class); + context.register(ConditionalBeanRegistrarConfiguration.class); + context.refresh(); + assertThat(context.containsBean("myTestBean")).isTrue(); + assertThat(context.getBean("myTestBean")).isInstanceOf(TestBean.class); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/left/LeftConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/left/LeftConfig.java index 2ff66f18cc54..617027c64630 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/left/LeftConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/left/LeftConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/right/RightConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/right/RightConfig.java index 73d529781ec3..e7ff629b4bb0 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/right/RightConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/right/RightConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/importing/ImportingConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/importing/ImportingConfig.java index 8177c06de06a..22980ed85220 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/importing/ImportingConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/importing/ImportingConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author 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.context.annotation.componentscan.importing; -import org.springframework.context.annotation.ComponentScanAndImportAnnotationInteractionTests; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportedConfig; @Configuration -@Import(ComponentScanAndImportAnnotationInteractionTests.ImportedConfig.class) +@Import(ImportedConfig.class) public class ImportingConfig { } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level1/Level1Config.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level1/Level1Config.java index bf817afa00ab..df0b51465e95 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level1/Level1Config.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level1/Level1Config.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level2/Level2Config.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level2/Level2Config.java index 3a69e5e564b8..deadb5b88d14 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level2/Level2Config.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level2/Level2Config.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level3/Level3Component.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level3/Level3Component.java index 4892450ec0b7..1ca7620aa17b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level3/Level3Component.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level3/Level3Component.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigA.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigA.java index 30a74d123cc3..b68e9719f0a5 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigA.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigA.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigB.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigB.java index c0dcf34e16bf..65f8f7aeb270 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigB.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigB.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigA.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigA.java index 64b92145c288..577c0b8d2e9e 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigA.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigA.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigB.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigB.java index d5749187dbb2..641b5fed8da8 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigB.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigB.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/ClassWithNestedComponents.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/ClassWithNestedComponents.java index 84bb476a4617..7c1686dcad4b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/ClassWithNestedComponents.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/ClassWithNestedComponents.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/SimpleComponent.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/SimpleComponent.java index 9eb57389f410..63342d3d90c3 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/SimpleComponent.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/SimpleComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java index 4ffe1f8106a0..32867ccea52b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -26,13 +27,16 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; -import org.springframework.beans.testfixture.beans.Colour; +import org.springframework.beans.testfixture.beans.Color; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -43,8 +47,10 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * System tests covering use of {@link Autowired} and {@link Value} within @@ -57,47 +63,57 @@ class AutowiredConfigurationTests { @Test - void testAutowiredConfigurationDependencies() { + void autowiredConfigurationDependencies() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( AutowiredConfigurationTests.class.getSimpleName() + ".xml", AutowiredConfigurationTests.class); - assertThat(context.getBean("colour", Colour.class)).isEqualTo(Colour.RED); - assertThat(context.getBean("testBean", TestBean.class).getName()).isEqualTo(Colour.RED.toString()); + assertThat(context.getBean("color", Color.class)).isEqualTo(Color.RED); + assertThat(context.getBean("testBean", TestBean.class).getName()).isEqualTo(Color.RED.toString()); context.close(); } @Test - void testAutowiredConfigurationMethodDependencies() { + void autowiredConfigurationMethodDependencies() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( AutowiredMethodConfig.class, ColorConfig.class); - assertThat(context.getBean(Colour.class)).isEqualTo(Colour.RED); + assertThat(context.getBean(Color.class)).isEqualTo(Color.RED); assertThat(context.getBean(TestBean.class).getName()).isEqualTo("RED-RED"); context.close(); } @Test - void testAutowiredConfigurationMethodDependenciesWithOptionalAndAvailable() { + void autowiredConfigurationMethodDependenciesWithOptionalAndAvailable() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( OptionalAutowiredMethodConfig.class, ColorConfig.class); - assertThat(context.getBean(Colour.class)).isEqualTo(Colour.RED); + assertThat(context.getBean(Color.class)).isEqualTo(Color.RED); assertThat(context.getBean(TestBean.class).getName()).isEqualTo("RED-RED"); context.close(); } @Test - void testAutowiredConfigurationMethodDependenciesWithOptionalAndNotAvailable() { + void autowiredConfigurationMethodDependenciesWithOptionalAndNotAvailable() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( OptionalAutowiredMethodConfig.class); - assertThat(context.getBeansOfType(Colour.class)).isEmpty(); + assertThat(context.getBeansOfType(Color.class)).isEmpty(); assertThat(context.getBean(TestBean.class).getName()).isEmpty(); context.close(); } @Test - void testAutowiredSingleConstructorSupported() { + void autowiredConfigurationMethodDependenciesWithQualifier() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + QualifiedAutowiredMethodConfig.class); + + assertThat(context.getBeansOfType(Color.class)).isEmpty(); + assertThat(context.getBean(TestBean.class).getName()).isEmpty(); + context.close(); + } + + @Test + void autowiredSingleConstructorSupported() { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions( new ClassPathResource("annotation-config.xml", AutowiredConstructorConfig.class)); @@ -105,12 +121,12 @@ void testAutowiredSingleConstructorSupported() { ctx.registerBeanDefinition("config1", new RootBeanDefinition(AutowiredConstructorConfig.class)); ctx.registerBeanDefinition("config2", new RootBeanDefinition(ColorConfig.class)); ctx.refresh(); - assertThat(ctx.getBean(Colour.class)).isSameAs(ctx.getBean(AutowiredConstructorConfig.class).colour); + assertThat(ctx.getBean(Color.class)).isSameAs(ctx.getBean(AutowiredConstructorConfig.class).color); ctx.close(); } @Test - void testObjectFactoryConstructorWithTypeVariable() { + void objectFactoryConstructorWithTypeVariable() { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions( new ClassPathResource("annotation-config.xml", ObjectFactoryConstructorConfig.class)); @@ -118,12 +134,12 @@ void testObjectFactoryConstructorWithTypeVariable() { ctx.registerBeanDefinition("config1", new RootBeanDefinition(ObjectFactoryConstructorConfig.class)); ctx.registerBeanDefinition("config2", new RootBeanDefinition(ColorConfig.class)); ctx.refresh(); - assertThat(ctx.getBean(Colour.class)).isSameAs(ctx.getBean(ObjectFactoryConstructorConfig.class).colour); + assertThat(ctx.getBean(Color.class)).isSameAs(ctx.getBean(ObjectFactoryConstructorConfig.class).color); ctx.close(); } @Test - void testAutowiredAnnotatedConstructorSupported() { + void autowiredAnnotatedConstructorSupported() { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions( new ClassPathResource("annotation-config.xml", MultipleConstructorConfig.class)); @@ -131,12 +147,12 @@ void testAutowiredAnnotatedConstructorSupported() { ctx.registerBeanDefinition("config1", new RootBeanDefinition(MultipleConstructorConfig.class)); ctx.registerBeanDefinition("config2", new RootBeanDefinition(ColorConfig.class)); ctx.refresh(); - assertThat(ctx.getBean(Colour.class)).isSameAs(ctx.getBean(MultipleConstructorConfig.class).colour); + assertThat(ctx.getBean(Color.class)).isSameAs(ctx.getBean(MultipleConstructorConfig.class).color); ctx.close(); } @Test - void testValueInjection() { + void valueInjection() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "ValueInjectionTests.xml", AutowiredConfigurationTests.class); doTestValueInjection(context); @@ -144,7 +160,7 @@ void testValueInjection() { } @Test - void testValueInjectionWithMetaAnnotation() { + void valueInjectionWithMetaAnnotation() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueConfigWithMetaAnnotation.class); doTestValueInjection(context); @@ -152,7 +168,7 @@ void testValueInjectionWithMetaAnnotation() { } @Test - void testValueInjectionWithAliasedMetaAnnotation() { + void valueInjectionWithAliasedMetaAnnotation() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueConfigWithAliasedMetaAnnotation.class); doTestValueInjection(context); @@ -160,7 +176,7 @@ void testValueInjectionWithAliasedMetaAnnotation() { } @Test - void testValueInjectionWithProviderFields() { + void valueInjectionWithProviderFields() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueConfigWithProviderFields.class); doTestValueInjection(context); @@ -168,7 +184,7 @@ void testValueInjectionWithProviderFields() { } @Test - void testValueInjectionWithProviderConstructorArguments() { + void valueInjectionWithProviderConstructorArguments() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueConfigWithProviderConstructorArguments.class); doTestValueInjection(context); @@ -176,13 +192,19 @@ void testValueInjectionWithProviderConstructorArguments() { } @Test - void testValueInjectionWithProviderMethodArguments() { + void valueInjectionWithProviderMethodArguments() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueConfigWithProviderMethodArguments.class); doTestValueInjection(context); context.close(); } + @Test + void valueInjectionWithAccidentalAutowiredAnnotations() { + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> + new AnnotationConfigApplicationContext(ValueConfigWithAccidentalAutowiredAnnotations.class)); + } + private void doTestValueInjection(BeanFactory context) { System.clearProperty("myProp"); @@ -210,7 +232,7 @@ private void doTestValueInjection(BeanFactory context) { } @Test - void testCustomPropertiesWithClassPathContext() throws IOException { + void customPropertiesWithClassPathContext() throws IOException { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "AutowiredConfigurationTests-custom.xml", AutowiredConfigurationTests.class); @@ -221,7 +243,7 @@ void testCustomPropertiesWithClassPathContext() throws IOException { } @Test - void testCustomPropertiesWithGenericContext() throws IOException { + void customPropertiesWithGenericContext() throws IOException { GenericApplicationContext context = new GenericApplicationContext(); new XmlBeanDefinitionReader(context).loadBeanDefinitions( new ClassPathResource("AutowiredConfigurationTests-custom.xml", AutowiredConfigurationTests.class)); @@ -234,7 +256,7 @@ void testCustomPropertiesWithGenericContext() throws IOException { } @Test - void testValueInjectionWithRecord() { + void valueInjectionWithRecord() { System.setProperty("recordBeanName", "enigma"); try (GenericApplicationContext context = new AnnotationConfigApplicationContext(RecordBean.class)) { assertThat(context.getBean(RecordBean.class).name()).isEqualTo("enigma"); @@ -253,11 +275,11 @@ private int contentLength() throws IOException { static class AutowiredConfig { @Autowired - private Colour colour; + private Color color; @Bean public TestBean testBean() { - return new TestBean(colour.toString()); + return new TestBean(color.toString()); } } @@ -266,8 +288,8 @@ public TestBean testBean() { static class AutowiredMethodConfig { @Bean - public TestBean testBean(Colour colour, List colours) { - return new TestBean(colour.toString() + "-" + colours.get(0).toString()); + public TestBean testBean(Color color, List colors) { + return new TestBean(color + "-" + colors.get(0)); } } @@ -276,25 +298,44 @@ public TestBean testBean(Colour colour, List colours) { static class OptionalAutowiredMethodConfig { @Bean - public TestBean testBean(Optional colour, Optional> colours) { - if (colour.isEmpty() && colours.isEmpty()) { + public TestBean testBean(Optional color, Optional> colors) { + if (color.isEmpty() && colors.isEmpty()) { return new TestBean(""); } else { - return new TestBean(colour.get() + "-" + colours.get().get(0).toString()); + return new TestBean(color.get() + "-" + colors.get().get(0)); } } } + @Configuration + static class QualifiedAutowiredMethodConfig { + + @Bean + @Qualifier("testBean") + public TestBean testBean(Optional color, Optional> colors) { + if (!color.isEmpty() || !colors.isEmpty()) { + throw new IllegalStateException("Unexpected match: " + color + " " + colors); + } + return new TestBean(""); + } + + @Bean + public List someList() { + return Collections.singletonList(new TestBean("shouldNotMatch")); + } + } + + @Configuration static class AutowiredConstructorConfig { - Colour colour; + Color color; // @Autowired - AutowiredConstructorConfig(Colour colour) { - this.colour = colour; + AutowiredConstructorConfig(Color color) { + this.color = color; } } @@ -302,11 +343,11 @@ static class AutowiredConstructorConfig { @Configuration static class ObjectFactoryConstructorConfig { - Colour colour; + Color color; // @Autowired - ObjectFactoryConstructorConfig(ObjectFactory colourFactory) { - this.colour = colourFactory.getObject(); + ObjectFactoryConstructorConfig(ObjectFactory colorFactory) { + this.color = colorFactory.getObject(); } } @@ -314,15 +355,15 @@ static class ObjectFactoryConstructorConfig { @Configuration static class MultipleConstructorConfig { - Colour colour; + Color color; @Autowired - MultipleConstructorConfig(Colour colour) { - this.colour = colour; + MultipleConstructorConfig(Color color) { + this.color = color; } MultipleConstructorConfig(String test) { - this.colour = new Colour(test); + this.color = Color.BLUE; } } @@ -331,8 +372,8 @@ static class MultipleConstructorConfig { static class ColorConfig { @Bean - public Colour colour() { - return Colour.RED; + public Color color() { + return Color.RED; } } @@ -494,6 +535,32 @@ public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider } + @Configuration + static class ValueConfigWithAccidentalAutowiredAnnotations implements InitializingBean { + + boolean invoked; + + @Override + public void afterPropertiesSet() { + Assert.state(!invoked, "Factory method must not get invoked on startup"); + } + + @Bean @Scope("prototype") + @Autowired + public TestBean testBean(@Value("#{systemProperties[myProp]}") Provider name) { + invoked = true; + return new TestBean(name.get()); + } + + @Bean @Scope("prototype") + @Autowired + public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider name2) { + invoked = true; + return new TestBean(name2.get()); + } + } + + @Configuration static class PropertiesConfig { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Bar.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Bar.java new file mode 100644 index 000000000000..bce5f84402e5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Bar.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.configuration; + +public class Bar { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java index fe3f8b4a69d6..90d553c52aeb 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java index 67bb4bc3b881..971fff37b5db 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -195,6 +195,28 @@ void customWithAttributeOverride() { ctx.close(); } + @Test + void customWithConstructor() { + AnnotationConfigApplicationContext ctx = context(CustomConfig.class, CustomPojoWithConstructor.class); + + CustomPojoWithConstructor pojo = ctx.getBean(CustomPojoWithConstructor.class); + assertThat(pojo.plainBean).isNull(); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + + ctx.close(); + } + + @Test + void customWithMethod() { + AnnotationConfigApplicationContext ctx = context(CustomConfig.class, CustomPojoWithMethod.class); + + CustomPojoWithMethod pojo = ctx.getBean(CustomPojoWithMethod.class); + assertThat(pojo.plainBean).isNull(); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + + ctx.close(); + } + @Test void beanNamesForAnnotation() { AnnotationConfigApplicationContext ctx = context(StandardConfig.class); @@ -327,6 +349,7 @@ public TestBean testBean2x() { } } + @Configuration static class EffectivePrimaryConfig { @@ -346,6 +369,7 @@ public TestBean fallback2() { } } + @Component @Lazy static class StandardPojo { @@ -418,6 +442,35 @@ public CustomPojo(Optional plainBean) { } + @InterestingPojo + static class CustomPojoWithConstructor { + + TestBean plainBean; + + TestBean testBean; + + public CustomPojoWithConstructor(Optional plainBean, @InterestingNeed TestBean testBean) { + this.plainBean = plainBean.orElse(null); + this.testBean = testBean; + } + } + + + @InterestingPojo + static class CustomPojoWithMethod { + + TestBean plainBean; + + TestBean testBean; + + @Autowired + public void applyDependencies(Optional plainBean, @InterestingNeed TestBean testBean) { + this.plainBean = plainBean.orElse(null); + this.testBean = testBean; + } + } + + @Qualifier @Retention(RetentionPolicy.RUNTIME) @interface Boring { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationBeanNameTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationBeanNameTests.java index e3010d10a5f0..27f16741338a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationBeanNameTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationBeanNameTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,8 +64,7 @@ void registerOuterConfig_withBeanNameGenerator() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.setBeanNameGenerator(new AnnotationBeanNameGenerator() { @Override - public String generateBeanName( - BeanDefinition definition, BeanDefinitionRegistry registry) { + public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { return "custom-" + super.generateBeanName(definition, registry); } }); @@ -78,17 +77,22 @@ public String generateBeanName( ctx.close(); } + @Configuration("outer") @Import(C.class) static class A { + @Component("nested") static class B { + @Bean public String nestedBean() { return ""; } } } + @Configuration("imported") static class C { + @Bean public String s() { return "s"; } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java index b79ab84bdd42..867a48479323 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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,13 @@ import org.aspectj.lang.annotation.Before; import org.junit.jupiter.api.Test; +import org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.IOther; +import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -32,10 +36,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Proxyable; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.io.ClassPathResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.context.annotation.ProxyType.INTERFACES; +import static org.springframework.context.annotation.ProxyType.TARGET_CLASS; /** * System tests covering use of AspectJ {@link Aspect}s in conjunction with {@link Configuration} classes. @@ -62,18 +69,40 @@ void configurationIncludesAspect() { assertAdviceWasApplied(ConfigurationWithAspect.class); } - private void assertAdviceWasApplied(Class configClass) { + @Test + void configurationIncludesAspectAndProxyable() { + assertAdviceWasApplied(ConfigurationWithAspectAndProxyable.class, TestBean.class); + } + + @Test + void configurationIncludesAspectAndProxyableInterfaces() { + assertAdviceWasApplied(ConfigurationWithAspectAndProxyableInterfaces.class, TestBean.class, Comparable.class); + } + + @Test + void configurationIncludesAspectAndProxyableTargetClass() { + assertAdviceWasApplied(ConfigurationWithAspectAndProxyableTargetClass.class); + } + + private void assertAdviceWasApplied(Class configClass, Class... notImplemented) { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(factory).loadBeanDefinitions( new ClassPathResource("aspectj-autoproxy-config.xml", ConfigurationClassAspectIntegrationTests.class)); GenericApplicationContext ctx = new GenericApplicationContext(factory); ctx.addBeanFactoryPostProcessor(new ConfigurationClassPostProcessor()); - ctx.registerBeanDefinition("config", new RootBeanDefinition(configClass)); + ctx.registerBeanDefinition("config", + new RootBeanDefinition(configClass, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); ctx.refresh(); - TestBean testBean = ctx.getBean("testBean", TestBean.class); + ITestBean testBean = ctx.getBean("testBean", ITestBean.class); + if (notImplemented.length > 0) { + assertThat(testBean).isNotInstanceOfAny(notImplemented); + } + else { + assertThat(testBean).isInstanceOf(TestBean.class); + } assertThat(testBean.getName()).isEqualTo("name"); - testBean.absquatulate(); + ((IOther) testBean).absquatulate(); assertThat(testBean.getName()).isEqualTo("advisedName"); ctx.close(); } @@ -120,6 +149,58 @@ public NameChangingAspect nameChangingAspect() { } + @Configuration + static class ConfigurationWithAspectAndProxyable { + + @Bean + @Proxyable(INTERFACES) + public TestBean testBean() { + return new TestBean("name"); + } + + @Bean + public NameChangingAspect nameChangingAspect() { + return new NameChangingAspect(); + } + } + + + @Configuration() + static class ConfigurationWithAspectAndProxyableInterfaces { + + @Bean + @Proxyable(interfaces = {ITestBean.class, IOther.class}) + public TestBean testBean() { + return new TestBean("name"); + } + + @Bean + public NameChangingAspect nameChangingAspect() { + return new NameChangingAspect(); + } + } + + + @Configuration + static class ConfigurationWithAspectAndProxyableTargetClass { + + public ConfigurationWithAspectAndProxyableTargetClass(AbstractAutoProxyCreator autoProxyCreator) { + autoProxyCreator.setProxyTargetClass(false); + } + + @Bean + @Proxyable(TARGET_CLASS) + public TestBean testBean() { + return new TestBean("name"); + } + + @Bean + public NameChangingAspect nameChangingAspect() { + return new NameChangingAspect(); + } + } + + @Aspect static class NameChangingAspect { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java index 7a860fb1739b..2ded0c1b98d4 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.ListFactoryBean; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.support.BeanDefinitionOverrideException; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.testfixture.beans.ITestBean; @@ -82,7 +83,7 @@ void customBeanNameIsRespectedWhenConfiguredViaValueAttribute() { () -> ConfigWithBeanWithCustomNameConfiguredViaValueAttribute.testBean, "enigma"); } - private void customBeanNameIsRespected(Class testClass, Supplier testBeanSupplier, String beanName) { + private static void customBeanNameIsRespected(Class testClass, Supplier testBeanSupplier, String beanName) { GenericApplicationContext ac = new GenericApplicationContext(); AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); ac.registerBeanDefinition("config", new RootBeanDefinition(testClass)); @@ -91,8 +92,8 @@ private void customBeanNameIsRespected(Class testClass, Supplier te assertThat(ac.getBean(beanName)).isSameAs(testBeanSupplier.get()); // method name should not be registered - assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> - ac.getBean("methodName")); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> ac.getBean("methodName")); } @Test @@ -109,14 +110,15 @@ void aliasesAreRespectedWhenConfiguredViaValueAttribute() { private void aliasesAreRespected(Class testClass, Supplier testBeanSupplier, String beanName) { TestBean testBean = testBeanSupplier.get(); - BeanFactory factory = initBeanFactory(testClass); + BeanFactory factory = initBeanFactory(false, testClass); assertThat(factory.getBean(beanName)).isSameAs(testBean); - Arrays.stream(factory.getAliases(beanName)).map(factory::getBean).forEach(alias -> assertThat(alias).isSameAs(testBean)); + assertThat(factory.getAliases(beanName)).extracting(factory::getBean) + .allMatch(alias -> alias == testBean); // method name should not be registered - assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> - factory.getBean("methodName")); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> factory.getBean("methodName")); } @Test // SPR-11830 @@ -139,31 +141,31 @@ void configWithSetWithProviderImplementation() { @Test void finalBeanMethod() { - assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> - initBeanFactory(ConfigWithFinalBean.class)); + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> initBeanFactory(false, ConfigWithFinalBean.class)); } @Test void finalBeanMethodWithoutProxy() { - initBeanFactory(ConfigWithFinalBeanWithoutProxy.class); + initBeanFactory(false, ConfigWithFinalBeanWithoutProxy.class); } @Test // gh-31007 void voidBeanMethod() { - assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> - initBeanFactory(ConfigWithVoidBean.class)); + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> initBeanFactory(false, ConfigWithVoidBean.class)); } @Test void simplestPossibleConfig() { - BeanFactory factory = initBeanFactory(SimplestPossibleConfig.class); + BeanFactory factory = initBeanFactory(false, SimplestPossibleConfig.class); String stringBean = factory.getBean("stringBean", String.class); assertThat(stringBean).isEqualTo("foo"); } @Test void configWithObjectReturnType() { - BeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class); + BeanFactory factory = initBeanFactory(false, ConfigWithNonSpecificReturnTypes.class); assertThat(factory.getType("stringBean")).isEqualTo(Object.class); assertThat(factory.isTypeMatch("stringBean", String.class)).isFalse(); String stringBean = factory.getBean("stringBean", String.class); @@ -172,35 +174,31 @@ void configWithObjectReturnType() { @Test void configWithFactoryBeanReturnType() { - ListableBeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class); + ListableBeanFactory factory = initBeanFactory(false, ConfigWithNonSpecificReturnTypes.class); assertThat(factory.getType("factoryBean")).isEqualTo(List.class); assertThat(factory.isTypeMatch("factoryBean", List.class)).isTrue(); assertThat(factory.getType("&factoryBean")).isEqualTo(FactoryBean.class); assertThat(factory.isTypeMatch("&factoryBean", FactoryBean.class)).isTrue(); assertThat(factory.isTypeMatch("&factoryBean", BeanClassLoaderAware.class)).isFalse(); assertThat(factory.isTypeMatch("&factoryBean", ListFactoryBean.class)).isFalse(); - boolean condition = factory.getBean("factoryBean") instanceof List; - assertThat(condition).isTrue(); + assertThat(factory.getBean("factoryBean")).isInstanceOf(List.class); String[] beanNames = factory.getBeanNamesForType(FactoryBean.class); - assertThat(beanNames).hasSize(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); + assertThat(beanNames).containsExactly("&factoryBean"); beanNames = factory.getBeanNamesForType(BeanClassLoaderAware.class); - assertThat(beanNames).hasSize(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); + assertThat(beanNames).containsExactly("&factoryBean"); beanNames = factory.getBeanNamesForType(ListFactoryBean.class); - assertThat(beanNames).hasSize(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); + assertThat(beanNames).containsExactly("&factoryBean"); beanNames = factory.getBeanNamesForType(List.class); - assertThat(beanNames[0]).isEqualTo("factoryBean"); + assertThat(beanNames).containsExactly("factoryBean"); } @Test void configurationWithPrototypeScopedBeans() { - BeanFactory factory = initBeanFactory(ConfigWithPrototypeBean.class); + BeanFactory factory = initBeanFactory(false, ConfigWithPrototypeBean.class); TestBean foo = factory.getBean("foo", TestBean.class); ITestBean bar = factory.getBean("bar", ITestBean.class); @@ -212,13 +210,27 @@ void configurationWithPrototypeScopedBeans() { @Test void configurationWithNullReference() { - BeanFactory factory = initBeanFactory(ConfigWithNullReference.class); + BeanFactory factory = initBeanFactory(false, ConfigWithNullReference.class); TestBean foo = factory.getBean("foo", TestBean.class); assertThat(factory.getBean("bar")).isEqualTo(null); assertThat(foo.getSpouse()).isNull(); } + @Test // gh-33330 + void configurationWithMethodNameMismatch() { + assertThatExceptionOfType(BeanDefinitionOverrideException.class) + .isThrownBy(() -> initBeanFactory(false, ConfigWithMethodNameMismatch.class)); + } + + @Test // gh-33920 + void configurationWithMethodNameMismatchAndOverridingAllowed() { + BeanFactory factory = initBeanFactory(true, ConfigWithMethodNameMismatch.class); + + SpousyTestBean foo = factory.getBean("foo", SpousyTestBean.class); + assertThat(foo.getName()).isIn("foo1", "foo2"); + } + @Test void configurationWithAdaptivePrototypes() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); @@ -255,7 +267,7 @@ void configurationWithAdaptiveResourcePrototypes() { void configurationWithPostProcessor() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithPostProcessor.class); - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "removal"}) RootBeanDefinition placeholderConfigurer = new RootBeanDefinition( org.springframework.beans.factory.config.PropertyPlaceholderConfigurer.class); placeholderConfigurer.getPropertyValues().add("properties", "myProp=myValue"); @@ -346,12 +358,13 @@ void autowiringWithDynamicPrototypeBeanClass() { * When complete, the factory is ready to service requests for any {@link Bean} methods * declared by {@code configClasses}. */ - private DefaultListableBeanFactory initBeanFactory(Class... configClasses) { + private DefaultListableBeanFactory initBeanFactory(boolean allowOverriding, Class... configClasses) { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); for (Class configClass : configClasses) { String configBeanName = configClass.getName(); factory.registerBeanDefinition(configBeanName, new RootBeanDefinition(configClass)); } + factory.setAllowBeanDefinitionOverriding(allowOverriding); ConfigurationClassPostProcessor ccpp = new ConfigurationClassPostProcessor(); ccpp.postProcessBeanDefinitionRegistry(factory); ccpp.postProcessBeanFactory(factory); @@ -365,7 +378,7 @@ static class ConfigWithBeanWithCustomName { static TestBean testBean = new TestBean(ConfigWithBeanWithCustomName.class.getSimpleName()); - @Bean(name = "customName") + @Bean("customName") public TestBean methodName() { return testBean; } @@ -389,7 +402,7 @@ static class ConfigWithBeanWithAliases { static TestBean testBean = new TestBean(ConfigWithBeanWithAliases.class.getSimpleName()); - @Bean(name = {"name1", "alias1", "alias2", "alias3"}) + @Bean({"name1", "alias1", "alias2", "alias3"}) public TestBean methodName() { return testBean; } @@ -414,7 +427,7 @@ static class ConfigWithBeanWithProviderImplementation implements Provider set = Collections.singleton("value"); @Override - @Bean(name = "customName") + @Bean("customName") public Set get() { return set; } @@ -437,7 +450,8 @@ public Set get() { @Configuration static class ConfigWithFinalBean { - @Bean public final TestBean testBean() { + @Bean + public final TestBean testBean() { return new TestBean(); } } @@ -446,7 +460,8 @@ static class ConfigWithFinalBean { @Configuration(proxyBeanMethods = false) static class ConfigWithFinalBeanWithoutProxy { - @Bean public final TestBean testBean() { + @Bean + public final TestBean testBean() { return new TestBean(); } } @@ -455,7 +470,8 @@ static class ConfigWithFinalBeanWithoutProxy { @Configuration static class ConfigWithVoidBean { - @Bean public void testBean() { + @Bean + public void testBean() { } } @@ -463,7 +479,8 @@ static class ConfigWithVoidBean { @Configuration static class SimplestPossibleConfig { - @Bean public String stringBean() { + @Bean + public String stringBean() { return "foo"; } } @@ -472,11 +489,13 @@ static class SimplestPossibleConfig { @Configuration static class ConfigWithNonSpecificReturnTypes { - @Bean public Object stringBean() { + @Bean + public Object stringBean() { return "foo"; } - @Bean public FactoryBean factoryBean() { + @Bean + public FactoryBean factoryBean() { ListFactoryBean fb = new ListFactoryBean(); fb.setSourceList(Arrays.asList("element1", "element2")); return fb; @@ -487,29 +506,34 @@ static class ConfigWithNonSpecificReturnTypes { @Configuration static class ConfigWithPrototypeBean { - @Bean public TestBean foo() { + @Bean + public TestBean foo() { TestBean foo = new SpousyTestBean("foo"); foo.setSpouse(bar()); return foo; } - @Bean public TestBean bar() { + @Bean + public TestBean bar() { TestBean bar = new SpousyTestBean("bar"); bar.setSpouse(baz()); return bar; } - @Bean @Scope("prototype") + @Bean + @Scope("prototype") public TestBean baz() { return new TestBean("baz"); } - @Bean @Scope("prototype") + @Bean + @Scope("prototype") public TestBean adaptive1(InjectionPoint ip) { return new TestBean(ip.getMember().getName()); } - @Bean @Scope("prototype") + @Bean + @Scope("prototype") public TestBean adaptive2(DependencyDescriptor dd) { return new TestBean(dd.getMember().getName()); } @@ -526,15 +550,33 @@ public TestBean bar() { } + @SuppressWarnings("deprecation") + @Configuration(enforceUniqueMethods = false) + static class ConfigWithMethodNameMismatch { + + @Bean("foo") + public TestBean foo1() { + return new SpousyTestBean("foo1"); + } + + @Bean("foo") + public TestBean foo2() { + return new SpousyTestBean("foo2"); + } + } + + @Scope("prototype") static class AdaptiveInjectionPoints { - @Autowired @Qualifier("adaptive1") + @Autowired + @Qualifier("adaptive1") public TestBean adaptiveInjectionPoint1; public TestBean adaptiveInjectionPoint2; - @Autowired @Qualifier("adaptive2") + @Autowired + @Qualifier("adaptive2") public void setAdaptiveInjectionPoint2(TestBean adaptiveInjectionPoint2) { this.adaptiveInjectionPoint2 = adaptiveInjectionPoint2; } @@ -658,15 +700,16 @@ public ApplicationListener listener() { } + @SuppressWarnings("deprecation") @Configuration(enforceUniqueMethods = false) public static class OverloadedBeanMismatch { - @Bean(name = "other") + @Bean("other") public NestedTestBean foo() { return new NestedTestBean(); } - @Bean(name = "foo") + @Bean("foo") public TestBean foo(@Qualifier("other") NestedTestBean other) { TestBean tb = new TestBean(); tb.setLawyer(other); @@ -699,7 +742,7 @@ static class AbstractPrototype implements PrototypeInterface { static class ConfigWithDynamicPrototype { @Bean - @Scope(value = "prototype") + @Scope("prototype") public PrototypeInterface getDemoBean(int i) { return switch (i) { case 1 -> new PrototypeOne(); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java index 30c2eb5d1be7..6ab7d1fcfe7b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ class ConfigurationClassWithPlaceholderConfigurerBeanTests { */ @Test @SuppressWarnings("resource") - public void valueFieldsAreNotProcessedWhenPlaceholderConfigurerIsIntegrated() { + void valueFieldsAreNotProcessedWhenPlaceholderConfigurerIsIntegrated() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithValueFieldAndPlaceholderConfigurer.class); System.setProperty("test.name", "foo"); @@ -75,7 +75,7 @@ public void valueFieldsAreNotProcessedWhenPlaceholderConfigurerIsIntegrated() { @Test @SuppressWarnings("resource") - public void valueFieldsAreProcessedWhenStaticPlaceholderConfigurerIsIntegrated() { + void valueFieldsAreProcessedWhenStaticPlaceholderConfigurerIsIntegrated() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithValueFieldAndStaticPlaceholderConfigurer.class); System.setProperty("test.name", "foo"); @@ -88,7 +88,7 @@ public void valueFieldsAreProcessedWhenStaticPlaceholderConfigurerIsIntegrated() @Test @SuppressWarnings("resource") - public void valueFieldsAreProcessedWhenPlaceholderConfigurerIsSegregated() { + void valueFieldsAreProcessedWhenPlaceholderConfigurerIsSegregated() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithValueField.class); ctx.register(ConfigWithPlaceholderConfigurer.class); @@ -102,7 +102,7 @@ public void valueFieldsAreProcessedWhenPlaceholderConfigurerIsSegregated() { @Test @SuppressWarnings("resource") - public void valueFieldsResolveToPlaceholderSpecifiedDefaultValuesWithPlaceholderConfigurer() { + void valueFieldsResolveToPlaceholderSpecifiedDefaultValuesWithPlaceholderConfigurer() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithValueField.class); ctx.register(ConfigWithPlaceholderConfigurer.class); @@ -114,7 +114,7 @@ public void valueFieldsResolveToPlaceholderSpecifiedDefaultValuesWithPlaceholder @Test @SuppressWarnings("resource") - public void valueFieldsResolveToPlaceholderSpecifiedDefaultValuesWithoutPlaceholderConfigurer() { + void valueFieldsResolveToPlaceholderSpecifiedDefaultValuesWithoutPlaceholderConfigurer() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(ConfigWithValueField.class); // ctx.register(ConfigWithPlaceholderConfigurer.class); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java index 4085cf39b447..b7505f173191 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java index 10d64f79d62a..b6aff2eaf714 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicateConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicateConfigurationClassPostProcessorTests.java index 5044190cac62..b67dc45dacc6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicateConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicateConfigurationClassPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicatePostProcessingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicatePostProcessingTests.java index 0b396be78964..9224f9203405 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicatePostProcessingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicatePostProcessingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportAnnotationDetectionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportAnnotationDetectionTests.java index 5addfbee632d..6c54efb12457 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportAnnotationDetectionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportAnnotationDetectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ * @since 3.1 */ @SuppressWarnings("resource") -public class ImportAnnotationDetectionTests { +class ImportAnnotationDetectionTests { @Test void multipleMetaImportsAreProcessed() { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java index 565b58e9d805..0257ca7772b6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.context.annotation.configuration; -import java.util.Collections; - import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.junit.jupiter.api.Test; @@ -25,18 +23,18 @@ import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.PropertySource; +import org.springframework.core.testfixture.env.MockPropertySource; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link ImportResource} support. + * Integration tests for {@link ImportResource @ImportResource} support. * * @author Chris Beams * @author Juergen Hoeller @@ -45,81 +43,88 @@ class ImportResourceTests { @Test - void importXml() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlConfig.class); - assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); - assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); - TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); - assertThat(tb.getName()).isEqualTo("myName"); - ctx.close(); + void importResource() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlConfig.class)) { + assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); + TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); + assertThat(tb.getName()).isEqualTo("myName"); + } } @Test - void importXmlIsInheritedFromSuperclassDeclarations() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(FirstLevelSubConfig.class); - assertThat(ctx.containsBean("xmlDeclaredBean")).isTrue(); - ctx.close(); + void importResourceIsInheritedFromSuperclassDeclarations() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(FirstLevelSubConfig.class)) { + assertThat(ctx.containsBean("xmlDeclaredBean")).isTrue(); + } } @Test - void importXmlIsMergedFromSuperclassDeclarations() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SecondLevelSubConfig.class); - assertThat(ctx.containsBean("secondLevelXmlDeclaredBean")).as("failed to pick up second-level-declared XML bean").isTrue(); - assertThat(ctx.containsBean("xmlDeclaredBean")).as("failed to pick up parent-declared XML bean").isTrue(); - ctx.close(); + void importResourceIsMergedFromSuperclassDeclarations() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SecondLevelSubConfig.class)) { + assertThat(ctx.containsBean("secondLevelXmlDeclaredBean")).as("failed to pick up second-level-declared XML bean").isTrue(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("failed to pick up parent-declared XML bean").isTrue(); + } } @Test - void importXmlWithNamespaceConfig() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithAopNamespaceConfig.class); - Object bean = ctx.getBean("proxiedXmlBean"); - assertThat(AopUtils.isAopProxy(bean)).isTrue(); - ctx.close(); + void importResourceWithNamespaceConfig() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithAopNamespaceConfig.class)) { + Object bean = ctx.getBean("proxiedXmlBean"); + assertThat(AopUtils.isAopProxy(bean)).isTrue(); + } } @Test - void importXmlWithOtherConfigurationClass() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithConfigurationClass.class); - assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); - assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); - TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); - assertThat(tb.getName()).isEqualTo("myName"); - ctx.close(); + void importResourceWithOtherConfigurationClass() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithConfigurationClass.class)) { + assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); + TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); + assertThat(tb.getName()).isEqualTo("myName"); + } } @Test void importWithPlaceholder() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - PropertySource propertySource = new MapPropertySource("test", - Collections. singletonMap("test", "springframework")); - ctx.getEnvironment().getPropertySources().addFirst(propertySource); - ctx.register(ImportXmlConfig.class); - ctx.refresh(); - assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); - ctx.close(); + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) { + ctx.getEnvironment().getPropertySources().addFirst(new MockPropertySource("test").withProperty("test", "springframework")); + ctx.register(ImportXmlConfig.class); + ctx.refresh(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); + } } @Test - void importXmlWithAutowiredConfig() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlAutowiredConfig.class); - String name = ctx.getBean("xmlBeanName", String.class); - assertThat(name).isEqualTo("xml.declared"); - ctx.close(); + void importResourceWithAutowiredConfig() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlAutowiredConfig.class)) { + String name = ctx.getBean("xmlBeanName", String.class); + assertThat(name).isEqualTo("xml.declared"); + } } @Test void importNonXmlResource() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportNonXmlResourceConfig.class); - assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue(); - ctx.close(); + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportNonXmlResourceConfig.class)) { + assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue(); + } + } + + @Test + void importResourceWithPrivateReader() { + try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportWithPrivateReaderConfig.class)) { + assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue(); + } } @Configuration @ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml") static class ImportXmlConfig { + @Value("${name}") private String name; + @Bean public TestBean javaDeclaredBean() { return new TestBean(this.name); } @@ -146,6 +151,7 @@ static class ImportXmlWithAopNamespaceConfig { @Aspect static class AnAspect { + @Before("execution(* org.springframework.beans.testfixture.beans.TestBean.*(..))") public void advice() { } } @@ -158,18 +164,37 @@ static class ImportXmlWithConfigurationClass { @Configuration @ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml") static class ImportXmlAutowiredConfig { - @Autowired TestBean xmlDeclaredBean; - @Bean public String xmlBeanName() { + @Autowired + TestBean xmlDeclaredBean; + + @Bean + public String xmlBeanName() { return xmlDeclaredBean.getName(); } } @SuppressWarnings("deprecation") @Configuration - @ImportResource(locations = "classpath:org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig-context.properties", + @ImportResource(locations = "org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig.properties", reader = org.springframework.beans.factory.support.PropertiesBeanDefinitionReader.class) static class ImportNonXmlResourceConfig { } + @SuppressWarnings("deprecation") + @Configuration + @ImportResource(locations = "org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig.properties", + reader = PrivatePropertiesBeanDefinitionReader.class) + static class ImportWithPrivateReaderConfig { + } + + @SuppressWarnings("deprecation") + private static class PrivatePropertiesBeanDefinitionReader + extends org.springframework.beans.factory.support.PropertiesBeanDefinitionReader { + + PrivatePropertiesBeanDefinitionReader(BeanDefinitionRegistry registry) { + super(registry); + } + } + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java index 80172e53808a..b948163e68e1 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,37 +34,16 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * System tests for {@link Import} annotation support. + * Integration tests for {@link Import @Import} support. * * @author Chris Beams * @author Juergen Hoeller + * @author Daeho Kwon */ class ImportTests { - private DefaultListableBeanFactory processConfigurationClasses(Class... classes) { - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.setAllowBeanDefinitionOverriding(false); - for (Class clazz : classes) { - beanFactory.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz)); - } - ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - pp.postProcessBeanFactory(beanFactory); - return beanFactory; - } - - private void assertBeanDefinitionCount(int expectedCount, Class... classes) { - DefaultListableBeanFactory beanFactory = processConfigurationClasses(classes); - assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(expectedCount); - beanFactory.preInstantiateSingletons(); - for (Class clazz : classes) { - beanFactory.getBean(clazz); - } - } - - // ------------------------------------------------------------------------ - @Test - void testProcessImportsWithAsm() { + void processImportsWithAsm() { int configClasses = 2; int beansInClasses = 2; DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @@ -75,29 +54,164 @@ void testProcessImportsWithAsm() { } @Test - void testProcessImportsWithDoubleImports() { + void processImportsWithDoubleImports() { int configClasses = 3; int beansInClasses = 3; assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class, OtherConfigurationWithImportAnnotation.class); } @Test - void testProcessImportsWithExplicitOverridingBefore() { + void processImportsWithExplicitOverridingBefore() { int configClasses = 2; int beansInClasses = 2; assertBeanDefinitionCount((configClasses + beansInClasses), OtherConfiguration.class, ConfigurationWithImportAnnotation.class); } @Test - void testProcessImportsWithExplicitOverridingAfter() { + void processImportsWithExplicitOverridingAfter() { int configClasses = 2; int beansInClasses = 2; assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class, OtherConfiguration.class); } + @Test + void importAnnotationWithTwoLevelRecursion() { + int configClasses = 2; + int beansInClasses = 3; + assertBeanDefinitionCount((configClasses + beansInClasses), AppConfig.class); + } + + @Test + void importAnnotationWithThreeLevelRecursion() { + int configClasses = 4; + int beansInClasses = 5; + assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class); + } + + @Test + void importAnnotationWithThreeLevelRecursionAndDoubleImport() { + int configClasses = 5; + int beansInClasses = 5; + assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class, FirstLevelPlus.class); + } + + @Test + void importAnnotationWithMultipleArguments() { + int configClasses = 3; + int beansInClasses = 3; + assertBeanDefinitionCount((configClasses + beansInClasses), WithMultipleArgumentsToImportAnnotation.class); + } + + @Test + void importAnnotationWithMultipleArgumentsResultingInOverriddenBeanDefinition() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setAllowBeanDefinitionOverriding(true); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition( + WithMultipleArgumentsThatWillCauseDuplication.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(4); + assertThat(beanFactory.getBean("foo", ITestBean.class).getName()).isEqualTo("foo2"); + } + + @Test + void importAnnotationOnInnerClasses() { + int configClasses = 2; + int beansInClasses = 2; + assertBeanDefinitionCount((configClasses + beansInClasses), OuterConfig.InnerConfig.class); + } + + @Test + void importNonConfigurationAnnotationClass() { + int configClasses = 2; + int beansInClasses = 0; + assertBeanDefinitionCount((configClasses + beansInClasses), ConfigAnnotated.class); + } + + /** + * Test that values supplied to @Configuration(value="...") are propagated as the + * bean name for the configuration class even in the case of inclusion via @Import + * or in the case of automatic registration via nesting + */ + @Test + void reproSpr9023() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(B.class); + assertThat(ctx.getBeanNamesForType(B.class)[0]).isEqualTo("config-b"); + assertThat(ctx.getBeanNamesForType(A.class)[0]).isEqualTo("config-a"); + ctx.close(); + } + + @Test + void processImports() { + int configClasses = 2; + int beansInClasses = 2; + assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class); + } + + /** + * An imported config must override a scanned one, thus bean definitions + * from the imported class is overridden by its importer. + */ + @Test // gh-24643 + void importedConfigOverridesScanned() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.setAllowBeanDefinitionOverriding(true); + ctx.scan(SiblingImportingConfigA.class.getPackage().getName()); + ctx.refresh(); + + assertThat(ctx.getBean("a-imports-b")).isEqualTo("valueFromA"); + assertThat(ctx.getBean("b-imports-a")).isEqualTo("valueFromBR"); + assertThat(ctx.getBeansOfType(SiblingImportingConfigA.class)).hasSize(1); + assertThat(ctx.getBeansOfType(SiblingImportingConfigB.class)).hasSize(1); + } + + @Test // gh-34820 + void importAnnotationOnImplementedInterfaceIsRespected() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(InterfaceBasedConfig.class); + + assertThat(context.getBean(ImportedConfig.class)).isNotNull(); + assertThat(context.getBean(ImportedBean.class)).hasFieldOrPropertyWithValue("name", "imported"); + + context.close(); + } + + @Test // gh-34820 + void localImportShouldOverrideInterfaceImport() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(OverridingConfig.class); + + assertThat(context.getBean(ImportedConfig.class)).isNotNull(); + assertThat(context.getBean(OverridingImportedConfig.class)).isNotNull(); + assertThat(context.getBean(ImportedBean.class)).hasFieldOrPropertyWithValue("name", "from class"); + + context.close(); + } + + + private static DefaultListableBeanFactory processConfigurationClasses(Class... classes) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setAllowBeanDefinitionOverriding(false); + for (Class clazz : classes) { + beanFactory.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz)); + } + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + return beanFactory; + } + + private static void assertBeanDefinitionCount(int expectedCount, Class... classes) { + DefaultListableBeanFactory beanFactory = processConfigurationClasses(classes); + assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(expectedCount); + beanFactory.preInstantiateSingletons(); + for (Class clazz : classes) { + beanFactory.getBean(clazz); + } + } + + @Configuration @Import(OtherConfiguration.class) static class ConfigurationWithImportAnnotation { + @Bean ITestBean one() { return new TestBean(); @@ -107,6 +221,7 @@ ITestBean one() { @Configuration @Import(OtherConfiguration.class) static class OtherConfigurationWithImportAnnotation { + @Bean ITestBean two() { return new TestBean(); @@ -115,21 +230,13 @@ ITestBean two() { @Configuration static class OtherConfiguration { + @Bean ITestBean three() { return new TestBean(); } } - // ------------------------------------------------------------------------ - - @Test - void testImportAnnotationWithTwoLevelRecursion() { - int configClasses = 2; - int beansInClasses = 3; - assertBeanDefinitionCount((configClasses + beansInClasses), AppConfig.class); - } - @Configuration @Import(DataSourceConfig.class) static class AppConfig { @@ -147,49 +254,13 @@ ITestBean accountRepository() { @Configuration static class DataSourceConfig { + @Bean ITestBean dataSourceA() { return new TestBean(); } } - // ------------------------------------------------------------------------ - - @Test - void testImportAnnotationWithThreeLevelRecursion() { - int configClasses = 4; - int beansInClasses = 5; - assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class); - } - - @Test - void testImportAnnotationWithThreeLevelRecursionAndDoubleImport() { - int configClasses = 5; - int beansInClasses = 5; - assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class, FirstLevelPlus.class); - } - - // ------------------------------------------------------------------------ - - @Test - void testImportAnnotationWithMultipleArguments() { - int configClasses = 3; - int beansInClasses = 3; - assertBeanDefinitionCount((configClasses + beansInClasses), WithMultipleArgumentsToImportAnnotation.class); - } - - @Test - void testImportAnnotationWithMultipleArgumentsResultingInOverriddenBeanDefinition() { - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.setAllowBeanDefinitionOverriding(true); - beanFactory.registerBeanDefinition("config", new RootBeanDefinition( - WithMultipleArgumentsThatWillCauseDuplication.class)); - ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - pp.postProcessBeanFactory(beanFactory); - assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(4); - assertThat(beanFactory.getBean("foo", ITestBean.class).getName()).isEqualTo("foo2"); - } - @Configuration @Import({Foo1.class, Foo2.class}) static class WithMultipleArgumentsThatWillCauseDuplication { @@ -197,6 +268,7 @@ static class WithMultipleArgumentsThatWillCauseDuplication { @Configuration static class Foo1 { + @Bean ITestBean foo() { return new TestBean("foo1"); @@ -205,23 +277,16 @@ ITestBean foo() { @Configuration static class Foo2 { + @Bean ITestBean foo() { return new TestBean("foo2"); } } - // ------------------------------------------------------------------------ - - @Test - void testImportAnnotationOnInnerClasses() { - int configClasses = 2; - int beansInClasses = 2; - assertBeanDefinitionCount((configClasses + beansInClasses), OuterConfig.InnerConfig.class); - } - @Configuration static class OuterConfig { + @Bean String whatev() { return "whatev"; @@ -239,17 +304,17 @@ ITestBean innerBean() { @Configuration static class ExternalConfig { + @Bean ITestBean extBean() { return new TestBean(); } } - // ------------------------------------------------------------------------ - @Configuration @Import(SecondLevel.class) static class FirstLevel { + @Bean TestBean m() { return new TestBean(); @@ -264,6 +329,7 @@ static class FirstLevelPlus { @Configuration @Import({ThirdLevel.class, InitBean.class}) static class SecondLevel { + @Bean TestBean n() { return new TestBean(); @@ -273,6 +339,7 @@ TestBean n() { @Configuration @DependsOn("org.springframework.context.annotation.configuration.ImportTests$InitBean") static class ThirdLevel { + ThirdLevel() { assertThat(InitBean.initialized).isTrue(); } @@ -294,7 +361,8 @@ ITestBean thirdLevelC() { } static class InitBean { - public static boolean initialized = false; + + static boolean initialized = false; InitBean() { initialized = true; @@ -304,6 +372,7 @@ static class InitBean { @Configuration @Import({LeftConfig.class, RightConfig.class}) static class WithMultipleArgumentsToImportAnnotation { + @Bean TestBean m() { return new TestBean(); @@ -312,6 +381,7 @@ TestBean m() { @Configuration static class LeftConfig { + @Bean ITestBean left() { return new TestBean(); @@ -320,44 +390,19 @@ ITestBean left() { @Configuration static class RightConfig { + @Bean ITestBean right() { return new TestBean(); } } - // ------------------------------------------------------------------------ - - @Test - void testImportNonConfigurationAnnotationClass() { - int configClasses = 2; - int beansInClasses = 0; - assertBeanDefinitionCount((configClasses + beansInClasses), ConfigAnnotated.class); - } - @Configuration @Import(NonConfigAnnotated.class) static class ConfigAnnotated { } static class NonConfigAnnotated { } - // ------------------------------------------------------------------------ - - /** - * Test that values supplied to @Configuration(value="...") are propagated as the - * bean name for the configuration class even in the case of inclusion via @Import - * or in the case of automatic registration via nesting - */ - @Test - void reproSpr9023() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(B.class); - ctx.refresh(); - assertThat(ctx.getBeanNamesForType(B.class)[0]).isEqualTo("config-b"); - assertThat(ctx.getBeanNamesForType(A.class)[0]).isEqualTo("config-a"); - ctx.close(); - } - @Configuration("config-a") static class A { } @@ -365,30 +410,38 @@ static class A { } @Import(A.class) static class B { } - // ------------------------------------------------------------------------ + record ImportedBean(String name) { + } - @Test - void testProcessImports() { - int configClasses = 2; - int beansInClasses = 2; - assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class); + @Configuration + static class ImportedConfig { + + @Bean + ImportedBean importedBean() { + return new ImportedBean("imported"); + } } - /** - * An imported config must override a scanned one, thus bean definitions - * from the imported class is overridden by its importer. - */ - @Test // gh-24643 - void importedConfigOverridesScanned() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.setAllowBeanDefinitionOverriding(true); - ctx.scan(SiblingImportingConfigA.class.getPackage().getName()); - ctx.refresh(); + @Configuration + static class OverridingImportedConfig { - assertThat(ctx.getBean("a-imports-b")).isEqualTo("valueFromA"); - assertThat(ctx.getBean("b-imports-a")).isEqualTo("valueFromBR"); - assertThat(ctx.getBeansOfType(SiblingImportingConfigA.class)).hasSize(1); - assertThat(ctx.getBeansOfType(SiblingImportingConfigB.class)).hasSize(1); + @Bean + ImportedBean importedBean() { + return new ImportedBean("from class"); + } + } + + @Import(ImportedConfig.class) + interface ConfigImportMarker { + } + + @Configuration + static class InterfaceBasedConfig implements ConfigImportMarker { + } + + @Configuration + @Import(OverridingImportedConfig.class) + static class OverridingConfig implements ConfigImportMarker { } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportWithConditionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportWithConditionTests.java index 0c54aabd753e..923d7103e237 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportWithConditionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportWithConditionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportedConfigurationClassEnhancementTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportedConfigurationClassEnhancementTests.java index 567d11bb58ba..a6bf151d1bf9 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportedConfigurationClassEnhancementTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportedConfigurationClassEnhancementTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/PackagePrivateBeanMethodInheritanceTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/PackagePrivateBeanMethodInheritanceTests.java index 91692d837bd9..fb99345444ec 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/PackagePrivateBeanMethodInheritanceTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/PackagePrivateBeanMethodInheritanceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 Chris Beams */ -public class PackagePrivateBeanMethodInheritanceTests { +class PackagePrivateBeanMethodInheritanceTests { @Test void repro() { @@ -64,9 +64,6 @@ public Foo(Bar bar) { } } - public static class Bar { - } - @Configuration public static class ReproConfig extends org.springframework.context.annotation.configuration.a.BaseConfig { @Bean diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ScopingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ScopingTests.java index 7c82bcc0e233..27c5cc8492b6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ScopingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ScopingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,12 +86,12 @@ private GenericApplicationContext createContext(Class configClass) { @Test - void testScopeOnClasses() { + void scopeOnClasses() { genericTestScope("scopedClass"); } @Test - void testScopeOnInterfaces() { + void scopeOnInterfaces() { genericTestScope("scopedInterface"); } @@ -130,7 +130,7 @@ private void genericTestScope(String beanName) { } @Test - void testSameScopeOnDifferentBeans() { + void sameScopeOnDifferentBeans() { Object beanAInScope = ctx.getBean("scopedClass"); Object beanBInScope = ctx.getBean("scopedInterface"); @@ -147,22 +147,20 @@ void testSameScopeOnDifferentBeans() { } @Test - void testRawScopes() { + void rawScopes() { String beanName = "scopedProxyInterface"; // get hidden bean Object bean = ctx.getBean("scopedTarget." + beanName); - boolean condition = bean instanceof ScopedObject; - assertThat(condition).isFalse(); + assertThat(bean).isNotInstanceOf(ScopedObject.class); } @Test - void testScopedProxyConfiguration() { + void scopedProxyConfiguration() { TestBean singleton = (TestBean) ctx.getBean("singletonWithScopedInterfaceDep"); ITestBean spouse = singleton.getSpouse(); - boolean condition = spouse instanceof ScopedObject; - assertThat(condition).as("scoped bean is not wrapped by the scoped-proxy").isTrue(); + assertThat(spouse).as("scoped bean is not wrapped by the scoped-proxy").isInstanceOf(ScopedObject.class); String beanName = "scopedProxyInterface"; @@ -191,11 +189,10 @@ void testScopedProxyConfiguration() { } @Test - void testScopedProxyConfigurationWithClasses() { + void scopedProxyConfigurationWithClasses() { TestBean singleton = (TestBean) ctx.getBean("singletonWithScopedClassDep"); ITestBean spouse = singleton.getSpouse(); - boolean condition = spouse instanceof ScopedObject; - assertThat(condition).as("scoped bean is not wrapped by the scoped-proxy").isTrue(); + assertThat(spouse).as("scoped bean is not wrapped by the scoped-proxy").isInstanceOf(ScopedObject.class); String beanName = "scopedProxyClass"; @@ -353,11 +350,6 @@ public Object get(String name, ObjectFactory objectFactory) { return beans.get(name); } - @Override - public String getConversationId() { - return null; - } - @Override public void registerDestructionCallback(String name, Runnable callback) { throw new IllegalStateException("Not supposed to be called"); @@ -368,10 +360,6 @@ public Object remove(String name) { return beans.remove(name); } - @Override - public Object resolveContextualObject(String key) { - return null; - } } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10668Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10668Tests.java index 2e57c5ebb5d2..4f37ab0524f5 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10668Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10668Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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 @@ class Spr10668Tests { @Test - void testSelfInjectHierarchy() { + void selfInjectHierarchy() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ChildConfig.class); assertThat(context.getBean(MyComponent.class)).isNotNull(); context.close(); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10744Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10744Tests.java index 1907f3ccec1b..f2bfbb07da11 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10744Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10744Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ class Spr10744Tests { @Test - void testSpr10744() { + void spr10744() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.getBeanFactory().registerScope("myTestScope", new MyTestScope()); context.register(MyTestConfiguration.class); @@ -83,16 +83,6 @@ public Object remove(String name) { @Override public void registerDestructionCallback(String name, Runnable callback) { } - - @Override - public Object resolveContextualObject(String key) { - return null; - } - - @Override - public String getConversationId() { - return null; - } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr12526Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr12526Tests.java index 5c4f58d5063e..245639889024 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr12526Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr12526Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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 @@ class Spr12526Tests { @Test - void testInjection() { + void injection() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TestContext.class); CustomCondition condition = ctx.getBean(CustomCondition.class); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr7167Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr7167Tests.java index 5e27c515833e..853e4564fab5 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr7167Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr7167Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/a/BaseConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/a/BaseConfig.java index 03fcb83cb5c8..07e5b25e7b6e 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/a/BaseConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/a/BaseConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author 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.context.annotation.configuration.a; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.configuration.PackagePrivateBeanMethodInheritanceTests.Bar; +import org.springframework.context.annotation.configuration.Bar; public abstract class BaseConfig { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Parent.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Parent.java index bc53b4833d3a..2c859c26518d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Parent.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Parent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Tests.java index 55f468638191..0fbbfd2b381a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/MarkerAnnotation.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/MarkerAnnotation.java new file mode 100644 index 000000000000..3ae0ff4a1be4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/MarkerAnnotation.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.configuration.spr9031; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface MarkerAnnotation { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java index 031be812eff1..fcab3eee9525 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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.context.annotation.configuration.spr9031; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -37,7 +34,7 @@ * @author Chris Beams * @since 3.1.1 */ -public class Spr9031Tests { +class Spr9031Tests { /** * Use of @Import to register LowLevelConfig results in ASM-based annotation @@ -76,7 +73,4 @@ static class LowLevelConfig { @Autowired Spr9031Component scanned; } - @Retention(RetentionPolicy.RUNTIME) - public @interface MarkerAnnotation {} - } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java index fcf57ef6110c..65efa26de4d7 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author 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.context.annotation.configuration.spr9031.scanpackage; -import org.springframework.context.annotation.configuration.spr9031.Spr9031Tests.MarkerAnnotation; +import org.springframework.context.annotation.configuration.spr9031.MarkerAnnotation; @MarkerAnnotation public class Spr9031Component { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java index f9d0574d552a..7f61aad9679f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,13 @@ package org.springframework.context.annotation.jsr330; -import junit.framework.Test; +import java.net.URI; +import java.util.Collections; +import java.util.stream.Stream; + +import junit.framework.TestCase; +import junit.framework.TestResult; +import junit.framework.TestSuite; import org.atinject.tck.Tck; import org.atinject.tck.auto.Car; import org.atinject.tck.auto.Convertible; @@ -28,20 +34,38 @@ import org.atinject.tck.auto.V8Engine; import org.atinject.tck.auto.accessories.Cupholder; import org.atinject.tck.auto.accessories.SpareTire; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.TestFactory; import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; import org.springframework.context.annotation.Jsr330ScopeMetadataResolver; import org.springframework.context.annotation.Primary; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.ClassUtils; + +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; /** + * {@code @Inject} Technology Compatibility Kit (TCK) tests. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 + * @see org.atinject.tck.Tck */ class SpringAtInjectTckTests { + @TestFactory + Stream runTechnologyCompatibilityKit() { + TestSuite testSuite = (TestSuite) Tck.testsFor(buildCar(), false, true); + Class suiteClass = resolveTestSuiteClass(testSuite); + return generateDynamicTests(testSuite, suiteClass); + } + + @SuppressWarnings("unchecked") - public static Test suite() { + private static Car buildCar() { GenericApplicationContext ac = new GenericApplicationContext(); AnnotatedBeanDefinitionReader bdr = new AnnotatedBeanDefinitionReader(ac); bdr.setScopeMetadataResolver(new Jsr330ScopeMetadataResolver()); @@ -56,9 +80,37 @@ public static Test suite() { bdr.registerBean(FuelTank.class); ac.refresh(); - Car car = ac.getBean(Car.class); + return ac.getBean(Car.class); + } + + private static Stream generateDynamicTests(TestSuite testSuite, Class suiteClass) { + return Collections.list(testSuite.tests()).stream().map(test -> { + if (test instanceof TestSuite nestedSuite) { + Class nestedSuiteClass = resolveTestSuiteClass(nestedSuite); + URI uri = URI.create("class:" + nestedSuiteClass.getName()); + return dynamicContainer(nestedSuite.getName(), uri, generateDynamicTests(nestedSuite, nestedSuiteClass)); + } + if (test instanceof TestCase testCase) { + URI uri = URI.create("method:" + suiteClass.getName() + "#" + testCase.getName()); + return dynamicTest(testCase.getName(), uri, () -> runTestCase(testCase)); + } + throw new IllegalStateException("Unsupported Test type: " + test.getClass().getName()); + }); + } + + private static void runTestCase(TestCase testCase) throws Throwable { + TestResult testResult = new TestResult(); + testCase.run(testResult); + if (testResult.failureCount() > 0) { + throw testResult.failures().nextElement().thrownException(); + } + if (testResult.errorCount() > 0) { + throw testResult.errors().nextElement().thrownException(); + } + } - return Tck.testsFor(car, false, true); + private static Class resolveTestSuiteClass(TestSuite testSuite) { + return ClassUtils.resolveClassName(testSuite.getName(), Tck.class.getClassLoader()); } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/lifecyclemethods/InitDestroyBean.java b/spring-context/src/test/java/org/springframework/context/annotation/lifecyclemethods/InitDestroyBean.java index 21c117c72bd7..2449e6f5fd49 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/lifecyclemethods/InitDestroyBean.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/lifecyclemethods/InitDestroyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/lifecyclemethods/PackagePrivateInitDestroyBean.java b/spring-context/src/test/java/org/springframework/context/annotation/lifecyclemethods/PackagePrivateInitDestroyBean.java index c37d60587267..f3bb7f9f3361 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/lifecyclemethods/PackagePrivateInitDestroyBean.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/lifecyclemethods/PackagePrivateInitDestroyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithRole.java b/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithRole.java index a4c17ca9fe9e..0e5e4672ed2d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithRole.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithRole.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithoutRole.java b/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithoutRole.java index 205b6ea3eca4..f4547b55370e 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithoutRole.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithoutRole.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ImportedConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ImportedConfig.java index bdf6cc436607..fb031b16a5ee 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ImportedConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ImportedConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentConfig.java index 84e8ec50c77a..ae0cfce52589 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithComponentScanConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithComponentScanConfig.java index 4ce2e03a1fdf..edbf96209aff 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithComponentScanConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithComponentScanConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportConfig.java index 10176d236a2e..1b171dfe6d85 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportResourceConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportResourceConfig.java index 80d8baf6f8ac..28927d394146 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportResourceConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportResourceConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithParentConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithParentConfig.java index 9efbec0f1ba9..8b24d882fa82 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithParentConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithParentConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/Spr10546Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/Spr10546Tests.java index ad7245354f2c..452276ce8f91 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/Spr10546Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/Spr10546Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/scanpackage/AEnclosingConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/scanpackage/AEnclosingConfig.java index 1d7b8585ccb1..9acaa6946067 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/scanpackage/AEnclosingConfig.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/scanpackage/AEnclosingConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr12111/TestProfileBean.java b/spring-context/src/test/java/org/springframework/context/annotation/spr12111/TestProfileBean.java index b4fc7526710c..41bc450db32a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr12111/TestProfileBean.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr12111/TestProfileBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr12233/Spr12233Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr12233/Spr12233Tests.java index 49f965c79938..5b72c9ec35d4 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr12233/Spr12233Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr12233/Spr12233Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr12334/Spr12334Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr12334/Spr12334Tests.java index ed9b70285a1d..18475fe81b2e 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr12334/Spr12334Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr12334/Spr12334Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScannedComponent.java b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScannedComponent.java index 5aae3afcd865..1ddeeeea722d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScannedComponent.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScannedComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScanningConfiguration.java b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScanningConfiguration.java index 229e8eb71a98..3171e9530b95 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScanningConfiguration.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScanningConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr16756/Spr16756Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/Spr16756Tests.java index b2595d086ea2..3b0e7f6eb8d5 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr16756/Spr16756Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/Spr16756Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr8761/Spr8761Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr8761/Spr8761Tests.java index ada7bcc09d23..7feb8c1f1c09 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr8761/Spr8761Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr8761/Spr8761Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr8808/Spr8808Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr8808/Spr8808Tests.java index 8b34767a095c..7475ad509e33 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/spr8808/Spr8808Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr8808/Spr8808Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation2/NamedStubDao2.java b/spring-context/src/test/java/org/springframework/context/annotation2/NamedStubDao2.java index 2296003adac5..b5e36bc5aa15 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation2/NamedStubDao2.java +++ b/spring-context/src/test/java/org/springframework/context/annotation2/NamedStubDao2.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation3/StubFooDao.java b/spring-context/src/test/java/org/springframework/context/annotation3/StubFooDao.java index 2aae57c201ef..d72eb03c13f3 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation3/StubFooDao.java +++ b/spring-context/src/test/java/org/springframework/context/annotation3/StubFooDao.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation4/DependencyBean.java b/spring-context/src/test/java/org/springframework/context/annotation4/DependencyBean.java index 38abca5f8274..744641c631b0 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation4/DependencyBean.java +++ b/spring-context/src/test/java/org/springframework/context/annotation4/DependencyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation4/FactoryMethodComponent.java b/spring-context/src/test/java/org/springframework/context/annotation4/FactoryMethodComponent.java index 743ad5773733..9c32a2a4a123 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation4/FactoryMethodComponent.java +++ b/spring-context/src/test/java/org/springframework/context/annotation4/FactoryMethodComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation4/SimpleBean.java b/spring-context/src/test/java/org/springframework/context/annotation4/SimpleBean.java index 69b5d94b393d..85a62b8b619f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation4/SimpleBean.java +++ b/spring-context/src/test/java/org/springframework/context/annotation4/SimpleBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation5/MyRepository.java b/spring-context/src/test/java/org/springframework/context/annotation5/MyRepository.java index 25e8fda04eb1..fc0f0c461391 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation5/MyRepository.java +++ b/spring-context/src/test/java/org/springframework/context/annotation5/MyRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation5/OtherFooDao.java b/spring-context/src/test/java/org/springframework/context/annotation5/OtherFooDao.java index 8ded77f865ea..60592bebb4c7 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation5/OtherFooDao.java +++ b/spring-context/src/test/java/org/springframework/context/annotation5/OtherFooDao.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation6/ComponentForScanning.java b/spring-context/src/test/java/org/springframework/context/annotation6/ComponentForScanning.java index 167ec8e0a2b5..2832bd493f3a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation6/ComponentForScanning.java +++ b/spring-context/src/test/java/org/springframework/context/annotation6/ComponentForScanning.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author 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,4 +20,5 @@ @Component public class ComponentForScanning { + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation6/ConfigForScanning.java b/spring-context/src/test/java/org/springframework/context/annotation6/ConfigForScanning.java index a95eafcce819..9bf010d41876 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation6/ConfigForScanning.java +++ b/spring-context/src/test/java/org/springframework/context/annotation6/ConfigForScanning.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,10 @@ @Configuration public class ConfigForScanning { + @Bean public TestBean testBean() { return new TestBean(); } + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation6/Jsr330NamedForScanning.java b/spring-context/src/test/java/org/springframework/context/annotation6/Jsr330NamedForScanning.java index e84bad2a5288..547d2667ec35 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation6/Jsr330NamedForScanning.java +++ b/spring-context/src/test/java/org/springframework/context/annotation6/Jsr330NamedForScanning.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/aot/AotApplicationContextInitializerTests.java b/spring-context/src/test/java/org/springframework/context/aot/AotApplicationContextInitializerTests.java index 8ae16093fe9c..755fee46db8d 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/AotApplicationContextInitializerTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/AotApplicationContextInitializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/aot/AotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/AotProcessorTests.java index ca82f6c7a88a..95c9195f4979 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/AotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/AotProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java index d3d67fd9b6c5..96afaa990ec6 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.context.aot; import java.io.IOException; -import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.List; import java.util.function.BiConsumer; @@ -40,6 +40,7 @@ import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.aot.AotProcessingException; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; @@ -50,7 +51,9 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.MethodReplacer; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.ReplaceOverride; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.beans.testfixture.beans.Pet; @@ -65,8 +68,10 @@ import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver; import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.context.testfixture.context.annotation.AutowiredCglibConfiguration; import org.springframework.context.testfixture.context.annotation.AutowiredComponent; import org.springframework.context.testfixture.context.annotation.AutowiredGenericTemplate; +import org.springframework.context.testfixture.context.annotation.AutowiredMixedCglibConfiguration; import org.springframework.context.testfixture.context.annotation.CglibConfiguration; import org.springframework.context.testfixture.context.annotation.ConfigurableCglibConfiguration; import org.springframework.context.testfixture.context.annotation.GenericTemplateConfiguration; @@ -78,9 +83,11 @@ import org.springframework.context.testfixture.context.annotation.LazyFactoryMethodArgumentComponent; import org.springframework.context.testfixture.context.annotation.LazyResourceFieldComponent; import org.springframework.context.testfixture.context.annotation.LazyResourceMethodComponent; +import org.springframework.context.testfixture.context.annotation.LookupComponent; import org.springframework.context.testfixture.context.annotation.PropertySourceConfiguration; import org.springframework.context.testfixture.context.annotation.QualifierConfiguration; import org.springframework.context.testfixture.context.annotation.ResourceComponent; +import org.springframework.context.testfixture.context.annotation.ValueCglibConfiguration; import org.springframework.context.testfixture.context.generator.SimpleComponent; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; @@ -94,6 +101,7 @@ import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link ApplicationContextAotGenerator}. @@ -107,6 +115,7 @@ class ApplicationContextAotGeneratorTests { void processAheadOfTimeWhenHasSimpleBean() { GenericApplicationContext applicationContext = new GenericApplicationContext(); applicationContext.registerBeanDefinition("test", new RootBeanDefinition(SimpleComponent.class)); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("test"); @@ -114,6 +123,99 @@ void processAheadOfTimeWhenHasSimpleBean() { }); } + @Test + void processAheadOfTimeWhenHasNoAotContributions() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).isEmpty(); + assertThat(compiled.getSourceFile()) + .contains("beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver())") + .contains("beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE)"); + }); + } + + @Test + void processAheadOfTimeWhenHasBeanFactoryInitializationAotProcessorExcludesProcessor() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition("test", + new RootBeanDefinition(NoOpBeanFactoryInitializationAotProcessor.class)); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).isEmpty(); + }); + } + + @Test + void processAheadOfTimeWhenHasBeanRegistrationAotProcessorExcludesProcessor() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition("test", + new RootBeanDefinition(NoOpBeanRegistrationAotProcessor.class)); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).isEmpty(); + }); + } + + @Test + void processAheadOfTimeWithPropertySource() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean(PropertySourceConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + ConfigurableEnvironment environment = freshApplicationContext.getEnvironment(); + PropertySource propertySource = environment.getPropertySources().get("testp1"); + assertThat(propertySource).isNotNull(); + assertThat(propertySource.getProperty("from.p1")).isEqualTo("p1Value"); + }); + } + + @Test + void processAheadOfTimeWithQualifier() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean(QualifierConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + QualifierConfiguration configuration = freshApplicationContext.getBean(QualifierConfiguration.class); + assertThat(configuration).hasFieldOrPropertyWithValue("bean", "one"); + }); + } + + @Test + void processAheadOfTimeWithInjectionPoint() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean(InjectionPointConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBean("classToString")) + .isEqualTo(InjectionPointConfiguration.class.getName()); + }); + } + + @Test // gh-30689 + void processAheadOfTimeWithExplicitResolvableType() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(One.class); + beanDefinition.setResolvedFactoryMethod(ReflectionUtils.findMethod(TestHierarchy.class, "oneBean")); + // Override target type + beanDefinition.setTargetType(Two.class); + beanFactory.registerBeanDefinition("hierarchyBean", beanDefinition); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBean(Two.class)) + .isInstanceOf(Implementation.class); + }); + } + + @Nested class Autowiring { @@ -124,6 +226,7 @@ void processAheadOfTimeWhenHasAutowiring() { AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME, AutowiredAnnotationBeanPostProcessor.class); applicationContext.registerBeanDefinition("autowiredComponent", new RootBeanDefinition(AutowiredComponent.class)); registerIntegerBean(applicationContext, "number", 42); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("autowiredComponent", "number"); @@ -133,11 +236,56 @@ void processAheadOfTimeWhenHasAutowiring() { }); } + @Test + void processAheadOfTimeWhenHasReplacer() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + registerBeanPostProcessor(applicationContext, + AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME, AutowiredAnnotationBeanPostProcessor.class); + RootBeanDefinition rbd = new RootBeanDefinition(AutowiredComponent.class); + rbd.getMethodOverrides().addOverride( + new ReplaceOverride("getCounter", "replacer")); + applicationContext.registerBeanDefinition("autowiredComponent", rbd); + registerIntegerBean(applicationContext, "number", 42); + applicationContext.registerBean("replacer", DummyReplacer.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("autowiredComponent", "number", "replacer"); + AutowiredComponent bean = freshApplicationContext.getBean(AutowiredComponent.class); + assertThat(bean.getEnvironment()).isSameAs(freshApplicationContext.getEnvironment()); + assertThat(bean.getCounter()).isEqualTo(44); + assertThat(bean.getCounter(0)).isEqualTo(42); + }); + } + + @Test + void processAheadOfTimeWhenHasLookup() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + registerBeanPostProcessor(applicationContext, + AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME, AutowiredAnnotationBeanPostProcessor.class); + RootBeanDefinition rbd = new RootBeanDefinition(LookupComponent.class); + rbd.getMethodOverrides().addOverride( + new ReplaceOverride("getCounter", "replacer", List.of( "Integer"))); + applicationContext.registerBeanDefinition("autowiredComponent", rbd); + registerIntegerBean(applicationContext, "number", 42); + applicationContext.registerBean("replacer", DummyReplacer.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("autowiredComponent", "number", "replacer"); + LookupComponent bean = freshApplicationContext.getBean(LookupComponent.class); + assertThat(bean.getEnvironment()).isSameAs(freshApplicationContext.getEnvironment()); + assertThat(bean.getCounter()).isEqualTo(42); + assertThat(bean.getCounter(0)).isEqualTo(44); + }); + } + @Test void processAheadOfTimeWhenHasAutowiringOnUnresolvedGeneric() { GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.registerBean(GenericTemplateConfiguration.class); applicationContext.registerBean("autowiredComponent", AutowiredGenericTemplate.class); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); AutowiredGenericTemplate bean = freshApplicationContext.getBean(AutowiredGenericTemplate.class); @@ -157,7 +305,6 @@ void processAheadOfTimeWhenHasLazyAutowiringOnField() { assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( environment.getClass().getInterfaces()))); - }); } @@ -221,15 +368,16 @@ private void testAutowiredComponent(Class type, RootBeanDefinition beanDe AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME, AutowiredAnnotationBeanPostProcessor.class); applicationContext.registerBeanDefinition("testComponent", beanDefinition); TestGenerationContext generationContext = processAheadOfTime(applicationContext); + testCompiledResult(generationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("testComponent"); assertions.accept(freshApplicationContext.getBean("testComponent", type), generationContext); }); } - } + @Nested class ResourceAutowiring { @@ -242,6 +390,7 @@ void processAheadOfTimeWhenHasResourceAutowiring() { registerStringBean(applicationContext, "text2", "hello2"); registerIntegerBean(applicationContext, "number", 42); applicationContext.registerBeanDefinition("resourceComponent", new RootBeanDefinition(ResourceComponent.class)); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("resourceComponent", "text", "text2", "number"); @@ -295,6 +444,7 @@ private void testResourceAutowiringComponent(Class type, RootBeanDefiniti AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, CommonAnnotationBeanPostProcessor.class); applicationContext.registerBeanDefinition("testComponent", beanDefinition); TestGenerationContext generationContext = processAheadOfTime(applicationContext); + testCompiledResult(generationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("testComponent"); @@ -303,6 +453,7 @@ private void testResourceAutowiringComponent(Class type, RootBeanDefiniti } } + @Nested class InitDestroy { @@ -313,6 +464,7 @@ void processAheadOfTimeWhenHasInitDestroyMethods() { AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, CommonAnnotationBeanPostProcessor.class); applicationContext.registerBeanDefinition("initDestroyComponent", new RootBeanDefinition(InitDestroyComponent.class)); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("initDestroyComponent"); @@ -332,6 +484,7 @@ void processAheadOfTimeWhenHasMultipleInitDestroyMethods() { beanDefinition.setInitMethodName("customInit"); beanDefinition.setDestroyMethodName("customDestroy"); applicationContext.registerBeanDefinition("initDestroyComponent", beanDefinition); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("initDestroyComponent"); @@ -341,105 +494,21 @@ void processAheadOfTimeWhenHasMultipleInitDestroyMethods() { assertThat(bean.events).containsExactly("init", "customInit", "destroy", "customDestroy"); }); } - } - @Test - void processAheadOfTimeWhenHasNoAotContributions() { - GenericApplicationContext applicationContext = new GenericApplicationContext(); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - assertThat(freshApplicationContext.getBeanDefinitionNames()).isEmpty(); - assertThat(compiled.getSourceFile()) - .contains("beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver())") - .contains("beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE)"); - }); - } - - @Test - void processAheadOfTimeWhenHasBeanFactoryInitializationAotProcessorExcludesProcessor() { - GenericApplicationContext applicationContext = new GenericApplicationContext(); - applicationContext.registerBeanDefinition("test", - new RootBeanDefinition(NoOpBeanFactoryInitializationAotProcessor.class)); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - assertThat(freshApplicationContext.getBeanDefinitionNames()).isEmpty(); - }); - } - - @Test - void processAheadOfTimeWhenHasBeanRegistrationAotProcessorExcludesProcessor() { - GenericApplicationContext applicationContext = new GenericApplicationContext(); - applicationContext.registerBeanDefinition("test", - new RootBeanDefinition(NoOpBeanRegistrationAotProcessor.class)); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - assertThat(freshApplicationContext.getBeanDefinitionNames()).isEmpty(); - }); - } - - - @Test - void processAheadOfTimeWithPropertySource() { - GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); - applicationContext.registerBean(PropertySourceConfiguration.class); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - ConfigurableEnvironment environment = freshApplicationContext.getEnvironment(); - PropertySource propertySource = environment.getPropertySources().get("testp1"); - assertThat(propertySource).isNotNull(); - assertThat(propertySource.getProperty("from.p1")).isEqualTo("p1Value"); - }); - } - - @Test - void processAheadOfTimeWithQualifier() { - GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); - applicationContext.registerBean(QualifierConfiguration.class); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - QualifierConfiguration configuration = freshApplicationContext.getBean(QualifierConfiguration.class); - assertThat(configuration).hasFieldOrPropertyWithValue("bean", "one"); - }); - } - - @Test - void processAheadOfTimeWithInjectionPoint() { - GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); - applicationContext.registerBean(InjectionPointConfiguration.class); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - assertThat(freshApplicationContext.getBean("classToString")) - .isEqualTo(InjectionPointConfiguration.class.getName()); - }); - } - - @Test // gh-30689 - void processAheadOfTimeWithExplicitResolvableType() { - GenericApplicationContext applicationContext = new GenericApplicationContext(); - DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); - RootBeanDefinition beanDefinition = new RootBeanDefinition(One.class); - beanDefinition.setResolvedFactoryMethod(ReflectionUtils.findMethod(TestHierarchy.class, "oneBean")); - // Override target type - beanDefinition.setTargetType(Two.class); - beanFactory.registerBeanDefinition("hierarchyBean", beanDefinition); - testCompiledResult(applicationContext, (initializer, compiled) -> { - GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); - assertThat(freshApplicationContext.getBean(Two.class)) - .isInstanceOf(Implementation.class); - }); - } @Nested @CompileWithForkedClassLoader class ConfigurationClassCglibProxy { + private static final String CGLIB_CONFIGURATION_CLASS_SUFFIX = "$$SpringCGLIB$$0"; + @Test void processAheadOfTimeWhenHasCglibProxyWriteProxyAndGenerateReflectionHints() throws IOException { GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.registerBean(CglibConfiguration.class); TestGenerationContext context = processAheadOfTime(applicationContext); - isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$0"); + isRegisteredCglibClass(context, CglibConfiguration.class.getName() + CGLIB_CONFIGURATION_CLASS_SUFFIX); isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$FastClass$$0"); isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$FastClass$$1"); } @@ -451,10 +520,48 @@ private void isRegisteredCglibClass(TestGenerationContext context, String cglibC .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(context.getRuntimeHints()); } + @Test + void processAheadOfTimeExposeUserClassForCglibProxy() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean("config", ValueCglibConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext).satisfies(hasBeanDefinitionOfBeanClass("config", ValueCglibConfiguration.class)); + assertThat(compiled.getSourceFile(".*ValueCglibConfiguration__BeanDefinitions")) + .contains("new RootBeanDefinition(ValueCglibConfiguration.class)") + .contains("new %s(".formatted(toCglibClassSimpleName(ValueCglibConfiguration.class))); + }); + } + + @Test + void processAheadOfTimeUsesCglibClassForFactoryMethod() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean("config", CglibConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext).satisfies(hasBeanDefinitionOfBeanClass("config", CglibConfiguration.class)); + assertThat(compiled.getSourceFile(".*CglibConfiguration__BeanDefinitions")) + .contains("new RootBeanDefinition(CglibConfiguration.class)") + .contains(">forFactoryMethod(%s.class,".formatted(toCglibClassSimpleName(CglibConfiguration.class))) + .doesNotContain(">forFactoryMethod(%s.class,".formatted(CglibConfiguration.class)); + }); + } + + private Consumer hasBeanDefinitionOfBeanClass(String name, Class beanClass) { + return context -> { + assertThat(context.containsBean(name)).isTrue(); + assertThat(context.getBeanDefinition(name)).isInstanceOfSatisfying(RootBeanDefinition.class, + rbd -> assertThat(rbd.getBeanClass()).isEqualTo(beanClass)); + }; + } + @Test void processAheadOfTimeWhenHasCglibProxyUseProxy() { GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.registerBean(CglibConfiguration.class); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); assertThat(freshApplicationContext.getBean("prefix", String.class)).isEqualTo("Hello0"); @@ -462,10 +569,55 @@ void processAheadOfTimeWhenHasCglibProxyUseProxy() { }); } + @Test + void processAheadOfTimeWhenHasCglibProxyAndAutowiring() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean(AutowiredCglibConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> { + context.setEnvironment(new MockEnvironment().withProperty("hello", "Hi")); + initializer.initialize(context); + }); + assertThat(freshApplicationContext.getBean("text", String.class)).isEqualTo("Hi World"); + }); + } + + @Test + void processAheadOfTimeWhenHasCglibProxyAndMixedAutowiring() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean(AutowiredMixedCglibConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> { + context.setEnvironment(new MockEnvironment().withProperty("hello", "Hi") + .withProperty("world", "AOT World")); + initializer.initialize(context); + }); + assertThat(freshApplicationContext.getBean("text", String.class)).isEqualTo("Hi AOT World"); + }); + } + + @Test + void processAheadOfTimeWhenHasCglibProxyWithAnnotationsOnTheUserClasConstructor() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean("config", ValueCglibConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> { + context.setEnvironment(new MockEnvironment().withProperty("name", "AOT World")); + initializer.initialize(context); + }); + assertThat(freshApplicationContext.getBean(ValueCglibConfiguration.class) + .getName()).isEqualTo("AOT World"); + }); + } + @Test void processAheadOfTimeWhenHasCglibProxyWithArgumentsUseProxy() { GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.registerBean(ConfigurableCglibConfiguration.class); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = createFreshApplicationContext(initializer); freshApplicationContext.setEnvironment(new MockEnvironment().withProperty("test.prefix", "Hi")); @@ -480,13 +632,16 @@ void processAheadOfTimeWhenHasCglibProxyWithArgumentsRegisterIntrospectionHintsO GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.registerBean(ConfigurableCglibConfiguration.class); TestGenerationContext generationContext = processAheadOfTime(applicationContext); - Constructor userConstructor = ConfigurableCglibConfiguration.class.getDeclaredConstructors()[0]; - assertThat(RuntimeHintsPredicates.reflection().onConstructor(userConstructor).introspect()) + assertThat(RuntimeHintsPredicates.reflection().onType(ConfigurableCglibConfiguration.class)) .accepts(generationContext.getRuntimeHints()); } + private String toCglibClassSimpleName(Class configClass) { + return configClass.getSimpleName() + CGLIB_CONFIGURATION_CLASS_SUFFIX; + } } + @Nested class ActiveProfile { @@ -497,6 +652,7 @@ void processAheadOfTimeWhenHasActiveProfiles(String[] aotProfiles, String[] runt if (aotProfiles.length != 0) { applicationContext.getEnvironment().setActiveProfiles(aotProfiles); } + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); if (runtimeProfiles.length != 0) { @@ -515,17 +671,18 @@ static Stream activeProfilesParameters() { Arguments.of(new String[] { "aot", "prod" }, new String[] { "aot", "prod" }, new String[] { "aot", "prod" }), Arguments.of(new String[] { "default" }, new String[] {}, new String[] {})); } - } + @Nested class XmlSupport { @Test void processAheadOfTimeWhenHasTypedStringValue() { GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); - applicationContext - .load(new ClassPathResource("applicationContextAotGeneratorTests-values.xml", getClass())); + applicationContext.load( + new ClassPathResource("applicationContextAotGeneratorTests-values.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); Employee employee = freshApplicationContext.getBean(Employee.class); @@ -542,8 +699,9 @@ void processAheadOfTimeWhenHasTypedStringValue() { @Test void processAheadOfTimeWhenHasTypedStringValueWithType() { GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); - applicationContext - .load(new ClassPathResource("applicationContextAotGeneratorTests-values-types.xml", getClass())); + applicationContext.load( + new ClassPathResource("applicationContextAotGeneratorTests-values-types.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); Employee employee = freshApplicationContext.getBean(Employee.class); @@ -558,8 +716,9 @@ void processAheadOfTimeWhenHasTypedStringValueWithType() { @Test void processAheadOfTimeWhenHasTypedStringValueWithExpression() { GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); - applicationContext - .load(new ClassPathResource("applicationContextAotGeneratorTests-values-expressions.xml", getClass())); + applicationContext.load( + new ClassPathResource("applicationContextAotGeneratorTests-values-expressions.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); Employee employee = freshApplicationContext.getBean(Employee.class); @@ -574,8 +733,9 @@ void processAheadOfTimeWhenHasTypedStringValueWithExpression() { @Test void processAheadOfTimeWhenXmlHasBeanReferences() { GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); - applicationContext - .load(new ClassPathResource("applicationContextAotGeneratorTests-references.xml", getClass())); + applicationContext.load( + new ClassPathResource("applicationContextAotGeneratorTests-references.xml", getClass())); + testCompiledResult(applicationContext, (initializer, compiled) -> { GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); assertThat(freshApplicationContext.getBean("petInnerBean", Pet.class) @@ -584,9 +744,26 @@ void processAheadOfTimeWhenXmlHasBeanReferences() { .getName()).isEqualTo("Dofi"); }); } + } + + @Nested + class ExceptionHandling { + + @Test + void failureProcessingBeanFactoryAotContribution() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition("test", + new RootBeanDefinition(FailingBeanFactoryInitializationAotContribution.class)); + assertThatExceptionOfType(AotProcessingException.class) + .isThrownBy(() -> processAheadOfTime(applicationContext)) + .withMessageStartingWith("Error executing '") + .withMessageContaining(FailingBeanFactoryInitializationAotContribution.class.getName()) + .withMessageContaining("Test exception"); + } } + private static void registerBeanPostProcessor(GenericApplicationContext applicationContext, String beanName, Class beanPostProcessorClass) { @@ -611,7 +788,7 @@ private static void registerIntegerBean(GenericApplicationContext applicationCon .getBeanDefinition()); } - private Consumer> doesNotHaveProxyFor(Class target) { + private static Consumer> doesNotHaveProxyFor(Class target) { return hints -> assertThat(hints).noneMatch(hint -> hint.getProxiedInterfaces().get(0).equals(TypeReference.of(target))); } @@ -626,18 +803,21 @@ private static TestGenerationContext processAheadOfTime(GenericApplicationContex private static void testCompiledResult(GenericApplicationContext applicationContext, BiConsumer, Compiled> result) { + testCompiledResult(processAheadOfTime(applicationContext), result); } - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings("unchecked") private static void testCompiledResult(TestGenerationContext generationContext, BiConsumer, Compiled> result) { + TestCompiler.forSystem().with(generationContext).compile(compiled -> result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled)); } private static GenericApplicationContext toFreshApplicationContext( ApplicationContextInitializer initializer) { + GenericApplicationContext freshApplicationContext = createFreshApplicationContext(initializer); freshApplicationContext.refresh(); return freshApplicationContext; @@ -645,6 +825,7 @@ private static GenericApplicationContext toFreshApplicationContext( private static GenericApplicationContext createFreshApplicationContext( ApplicationContextInitializer initializer) { + GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); initializer.initialize(freshApplicationContext); return freshApplicationContext; @@ -662,7 +843,6 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { return null; } - } @@ -673,7 +853,24 @@ static class NoOpBeanRegistrationAotProcessor public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { return null; } + } + + + static class FailingBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotProcessor { + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + throw new IllegalStateException("Test exception"); + } + } + + public static class DummyReplacer implements MethodReplacer { + + @Override + public Object reimplement(Object obj, Method method, Object[] args) throws Throwable { + return 44; + } } } diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextInitializationCodeGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextInitializationCodeGeneratorTests.java index 042658487bb1..a1ac0677af32 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextInitializationCodeGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextInitializationCodeGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/aot/CglibClassHandlerTests.java b/spring-context/src/test/java/org/springframework/context/aot/CglibClassHandlerTests.java index f814c2e011d5..1cda80de238e 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/CglibClassHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/CglibClassHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java index 384f54d59fcc..e24071796c2e 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,8 +45,9 @@ class ContextAotProcessorTests { void processGeneratesAssets(@TempDir Path directory) { GenericApplicationContext context = new AnnotationConfigApplicationContext(); context.registerBean(SampleApplication.class); - ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory); + DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory); ClassName className = processor.process(); + assertThat(processor.context.isClosed()).isTrue(); assertThat(className).isEqualTo(ClassName.get(SampleApplication.class.getPackageName(), "ContextAotProcessorTests_SampleApplication__ApplicationContextInitializer")); assertThat(directory).satisfies(hasGeneratedAssetsForSampleApplication()); @@ -61,9 +62,10 @@ void processingDeletesExistingOutput(@TempDir Path directory) throws IOException Path existingSourceOutput = createExisting(sourceOutput); Path existingResourceOutput = createExisting(resourceOutput); Path existingClassOutput = createExisting(classOutput); - ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, + DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, sourceOutput, resourceOutput, classOutput); processor.process(); + assertThat(processor.context.isClosed()).isTrue(); assertThat(existingSourceOutput).doesNotExist(); assertThat(existingResourceOutput).doesNotExist(); assertThat(existingClassOutput).doesNotExist(); @@ -73,13 +75,14 @@ void processingDeletesExistingOutput(@TempDir Path directory) throws IOException void processWithEmptyNativeImageArgumentsDoesNotCreateNativeImageProperties(@TempDir Path directory) { GenericApplicationContext context = new AnnotationConfigApplicationContext(); context.registerBean(SampleApplication.class); - ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) { + DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) { @Override protected List getDefaultNativeImageArguments(String application) { return Collections.emptyList(); } }; processor.process(); + assertThat(processor.context.isClosed()).isTrue(); assertThat(directory.resolve("resource/META-INF/native-image/com.example/example/native-image.properties")) .doesNotExist(); context.close(); @@ -102,15 +105,13 @@ private Consumer hasGeneratedAssetsForSampleApplication() { assertThat(directory.resolve( "source/org/springframework/context/aot/ContextAotProcessorTests_SampleApplication__BeanFactoryRegistrations.java")) .exists().isRegularFile(); - assertThat(directory.resolve("resource/META-INF/native-image/com.example/example/reflect-config.json")) + assertThat(directory.resolve("resource/META-INF/native-image/com.example/example/reachability-metadata.json")) .exists().isRegularFile(); Path nativeImagePropertiesFile = directory .resolve("resource/META-INF/native-image/com.example/example/native-image.properties"); assertThat(nativeImagePropertiesFile).exists().isRegularFile().hasContent(""" Args = -H:Class=org.springframework.context.aot.ContextAotProcessorTests$SampleApplication \\ - --report-unsupported-elements-at-runtime \\ - --no-fallback \\ - --install-exit-handlers + --no-fallback """); }; } @@ -118,6 +119,8 @@ private Consumer hasGeneratedAssetsForSampleApplication() { private static class DemoContextAotProcessor extends ContextAotProcessor { + AnnotationConfigApplicationContext context; + DemoContextAotProcessor(Class application, Path rootPath) { this(application, rootPath.resolve("source"), rootPath.resolve("resource"), rootPath.resolve("class")); } @@ -141,11 +144,12 @@ private static Settings createSettings(Path sourceOutput, Path resourceOutput, protected GenericApplicationContext prepareApplicationContext(Class application) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(application); + this.context = context; return context; } - } + @Configuration(proxyBeanMethods = false) static class SampleApplication { @@ -153,7 +157,6 @@ static class SampleApplication { public String testBean() { return "Hello"; } - } } diff --git a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java new file mode 100644 index 000000000000..8b1022a60cf6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.List; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.ObjectArrayAssert; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.context.testfixture.context.aot.scan.noreflective.ReflectiveNotUsed; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnConstructor; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnField; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnInnerField; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnInterface; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnMethod; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnNestedType; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnRecord; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.Reflective2OnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21.Reflective21OnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective22.Reflective22OnType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReflectiveProcessorAotContributionBuilder}. + * + * @author Stephane Nicoll + */ +class ReflectiveProcessorAotContributionBuilderTests { + + @Test + void classesWithMatchingCandidates() { + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .withClasses(List.of(String.class, ReflectiveOnInterface.class, Integer.class)).build(); + assertDetectedClasses(contribution).containsOnly(ReflectiveOnInterface.class).hasSize(1); + } + + @Test + void classesWithMatchingCandidatesFiltersDuplicates() { + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .withClasses(List.of(ReflectiveOnField.class, ReflectiveOnInterface.class, Integer.class)) + .withClasses(new Class[] { ReflectiveOnInterface.class, ReflectiveOnMethod.class, String.class }) + .build(); + assertDetectedClasses(contribution) + .containsOnly(ReflectiveOnInterface.class, ReflectiveOnField.class, ReflectiveOnMethod.class) + .hasSize(3); + } + + @Test + void scanWithMatchingCandidates() { + String packageName = ReflectiveOnType.class.getPackageName(); + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .scan(getClass().getClassLoader(), packageName).build(); + assertDetectedClasses(contribution).containsOnly(ReflectiveOnType.class, ReflectiveOnInterface.class, + ReflectiveOnRecord.class, ReflectiveOnField.class, ReflectiveOnConstructor.class, + ReflectiveOnMethod.class, ReflectiveOnNestedType.Nested.class, ReflectiveOnInnerField.Inner.class); + } + + @Test + void scanWithMatchingCandidatesInSubPackages() { + String packageName = Reflective2OnType.class.getPackageName(); + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .scan(getClass().getClassLoader(), packageName).build(); + assertDetectedClasses(contribution).containsOnly(Reflective2OnType.class, + Reflective21OnType.class, Reflective22OnType.class); + } + + @Test + void scanWithNoCandidate() { + String packageName = ReflectiveNotUsed.class.getPackageName(); + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .scan(getClass().getClassLoader(), packageName).build(); + assertThat(contribution).isNull(); + } + + @Test + void classesAndScanWithDuplicatesFiltersThem() { + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .withClasses(List.of(ReflectiveOnField.class, ReflectiveOnInterface.class, Integer.class)) + .withClasses(new Class[] { ReflectiveOnInterface.class, ReflectiveOnMethod.class, String.class }) + .scan(null, ReflectiveOnType.class.getPackageName()) + .build(); + assertDetectedClasses(contribution) + .containsOnly(ReflectiveOnType.class, ReflectiveOnInterface.class, ReflectiveOnRecord.class, + ReflectiveOnField.class, ReflectiveOnConstructor.class, ReflectiveOnMethod.class, + ReflectiveOnNestedType.Nested.class, ReflectiveOnInnerField.Inner.class) + .hasSize(8); + } + + @SuppressWarnings("rawtypes") + private ObjectArrayAssert assertDetectedClasses(@Nullable BeanFactoryInitializationAotContribution contribution) { + assertThat(contribution).isNotNull(); + return assertThat(contribution).extracting("classes", InstanceOfAssertFactories.array(Class[].class)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java index 950950eed9cb..907ebff6f459 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author 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.context.aot; -import java.lang.reflect.Constructor; - import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.TypeHint; +import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.annotation.Reflective; import org.springframework.aot.hint.predicate.ReflectionHintsPredicates; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; @@ -30,6 +30,10 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.ReflectiveScan; +import org.springframework.context.testfixture.context.aot.scan.reflective2.Reflective2OnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21.Reflective21OnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective22.Reflective22OnType; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -63,11 +67,52 @@ void shouldProcessAnnotationOnType() { void shouldProcessAllBeans() throws NoSuchMethodException { ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection(); process(SampleTypeAnnotatedBean.class, SampleConstructorAnnotatedBean.class); - Constructor constructor = SampleConstructorAnnotatedBean.class.getDeclaredConstructor(String.class); - assertThat(reflection.onType(SampleTypeAnnotatedBean.class).and(reflection.onConstructor(constructor))) + assertThat(reflection.onType(SampleTypeAnnotatedBean.class)) .accepts(this.generationContext.getRuntimeHints()); } + @Test + void shouldTriggerScanningIfBeanUsesReflectiveScan() { + process(SampleBeanWithReflectiveScan.class); + assertThat(this.generationContext.getRuntimeHints().reflection().typeHints().map(TypeHint::getType)) + .containsExactlyInAnyOrderElementsOf(TypeReference.listOf( + Reflective2OnType.class, Reflective21OnType.class, Reflective22OnType.class)); + } + + @Test + void findBasePackagesToScanWhenNoCandidateIsEmpty() { + Class[] candidates = { String.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)).isEmpty(); + } + + @Test + void findBasePackagesToScanWithBasePackageClasses() { + Class[] candidates = { SampleBeanWithReflectiveScan.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)) + .containsOnly(Reflective2OnType.class.getPackageName()); + } + + @Test + void findBasePackagesToScanWithBasePackages() { + Class[] candidates = { SampleBeanWithReflectiveScanWithName.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)) + .containsOnly(Reflective2OnType.class.getPackageName()); + } + + @Test + void findBasePackagesToScanWithBasePackagesAndClasses() { + Class[] candidates = { SampleBeanWithMultipleReflectiveScan.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)) + .containsOnly(Reflective21OnType.class.getPackageName(), Reflective22OnType.class.getPackageName()); + } + + @Test + void findBasePackagesToScanWithDuplicatesFiltersThem() { + Class[] candidates = { SampleBeanWithReflectiveScan.class, SampleBeanWithReflectiveScanWithName.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)) + .containsOnly(Reflective2OnType.class.getPackageName()); + } + private void process(Class... beanClasses) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); for (Class beanClass : beanClasses) { @@ -103,4 +148,17 @@ static class SampleConstructorAnnotatedBean { } + @ReflectiveScan(basePackageClasses = Reflective2OnType.class) + static class SampleBeanWithReflectiveScan { + } + + @ReflectiveScan("org.springframework.context.testfixture.context.aot.scan.reflective2") + static class SampleBeanWithReflectiveScanWithName { + } + + @ReflectiveScan(basePackageClasses = Reflective22OnType.class, + basePackages = "org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21") + static class SampleBeanWithMultipleReflectiveScan { + } + } diff --git a/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java index c3542b630fd8..98d83deee308 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,16 +32,16 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.factory.aot.AotProcessingException; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link RuntimeHintsBeanFactoryInitializationAotProcessor}. @@ -119,9 +120,9 @@ void shouldProcessDuplicatedRegistrarsOnlyOnce() { void shouldRejectRuntimeHintsRegistrarWithoutDefaultConstructor() { GenericApplicationContext applicationContext = createApplicationContext( ConfigurationWithIllegalRegistrar.class); - assertThatThrownBy(() -> this.generator.processAheadOfTime( - applicationContext, this.generationContext)) - .isInstanceOf(BeanInstantiationException.class); + assertThatExceptionOfType(AotProcessingException.class) + .isThrownBy(() -> this.generator.processAheadOfTime(applicationContext, this.generationContext)) + .havingCause().isInstanceOf(BeanInstantiationException.class); } private void assertThatSampleRegistrarContributed() { diff --git a/spring-context/src/test/java/org/springframework/context/aot/SampleJavaBean.java b/spring-context/src/test/java/org/springframework/context/aot/SampleJavaBean.java index 313125ad4ef7..09c2395ffde9 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/SampleJavaBean.java +++ b/spring-context/src/test/java/org/springframework/context/aot/SampleJavaBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java index 532135b383ae..dc4b87a99bc8 100644 --- a/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/conversionservice/Bar.java b/spring-context/src/test/java/org/springframework/context/conversionservice/Bar.java index 772dba18ee0f..39ecb3cfe35b 100644 --- a/spring-context/src/test/java/org/springframework/context/conversionservice/Bar.java +++ b/spring-context/src/test/java/org/springframework/context/conversionservice/Bar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/conversionservice/ConversionServiceContextConfigTests.java b/spring-context/src/test/java/org/springframework/context/conversionservice/ConversionServiceContextConfigTests.java index a594b76a9026..e56f0a892384 100644 --- a/spring-context/src/test/java/org/springframework/context/conversionservice/ConversionServiceContextConfigTests.java +++ b/spring-context/src/test/java/org/springframework/context/conversionservice/ConversionServiceContextConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-present the original author 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 @@ class ConversionServiceContextConfigTests { @Test - void testConfigOk() { + void configOk() { try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("org/springframework/context/conversionservice/conversionService.xml")) { TestClient client = context.getBean("testClient", TestClient.class); assertThat(client.getBars()).hasSize(2); diff --git a/spring-context/src/test/java/org/springframework/context/conversionservice/StringToBarConverter.java b/spring-context/src/test/java/org/springframework/context/conversionservice/StringToBarConverter.java index 747d593e1dd8..78774a15271d 100644 --- a/spring-context/src/test/java/org/springframework/context/conversionservice/StringToBarConverter.java +++ b/spring-context/src/test/java/org/springframework/context/conversionservice/StringToBarConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/conversionservice/TestClient.java b/spring-context/src/test/java/org/springframework/context/conversionservice/TestClient.java index 01fe855eb8f1..2c01f95fe729 100644 --- a/spring-context/src/test/java/org/springframework/context/conversionservice/TestClient.java +++ b/spring-context/src/test/java/org/springframework/context/conversionservice/TestClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2006 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java index 22d33a109cb9..924854fe1897 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java index a52c27d11267..237986b0aabd 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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.event; +import java.io.Serializable; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -105,8 +106,9 @@ void simpleEventJavaConfig() { this.eventCollector.assertTotalEventsCount(1); this.eventCollector.clear(); - this.context.publishEvent(event); - this.eventCollector.assertEvent(listener, event); + TestEvent otherEvent = new TestEvent(this, Integer.valueOf(1)); + this.context.publishEvent(otherEvent); + this.eventCollector.assertEvent(listener, otherEvent); this.eventCollector.assertTotalEventsCount(1); context.getBean(ApplicationEventMulticaster.class).removeApplicationListeners(l -> @@ -279,25 +281,6 @@ void collectionReplyNullValue() { this.eventCollector.assertTotalEventsCount(2); } - @Test - @SuppressWarnings("deprecation") - void listenableFutureReply() { - load(TestEventListener.class, ReplyEventListener.class); - org.springframework.util.concurrent.SettableListenableFuture future = - new org.springframework.util.concurrent.SettableListenableFuture<>(); - future.set("dummy"); - AnotherTestEvent event = new AnotherTestEvent(this, future); - ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); - TestEventListener listener = this.context.getBean(TestEventListener.class); - - this.eventCollector.assertNoEventReceived(listener); - this.eventCollector.assertNoEventReceived(replyEventListener); - this.context.publishEvent(event); - this.eventCollector.assertEvent(replyEventListener, event); - this.eventCollector.assertEvent(listener, "dummy"); // reply - this.eventCollector.assertTotalEventsCount(2); - } - @Test void completableFutureReply() { load(TestEventListener.class, ReplyEventListener.class); @@ -742,6 +725,11 @@ public void handle(TestEvent event) { public void handleString(String content) { collectEvent(content); } + + @EventListener({Boolean.class, Integer.class}) + public void handleBooleanOrInteger(Serializable content) { + collectEvent(content); + } } @@ -1009,6 +997,8 @@ interface ConditionalEventInterface extends Identifiable { void handleString(String payload); + void handleBooleanOrInteger(Serializable content); + void handleTimestamp(Long timestamp); void handleRatio(Double ratio); @@ -1031,6 +1021,12 @@ public void handleString(String payload) { super.handleString(payload); } + @EventListener({Boolean.class, Integer.class}) + @Override + public void handleBooleanOrInteger(Serializable content) { + super.handleBooleanOrInteger(content); + } + @ConditionalEvent("#root.event.timestamp > #p0") @Override public void handleTimestamp(Long timestamp) { @@ -1109,16 +1105,6 @@ public Object remove(String name) { @Override public void registerDestructionCallback(String name, Runnable callback) { } - - @Override - public Object resolveContextualObject(String key) { - return null; - } - - @Override - public String getConversationId() { - return null; - } } diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java index 184ccdb22efb..fcf66e9d639d 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-present the original author 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,13 @@ import java.util.function.Consumer; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanPostProcessor; @@ -59,6 +61,7 @@ import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatRuntimeException; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.BDDMockito.given; @@ -245,7 +248,7 @@ void orderedListenersWithAnnotation() { @Test @SuppressWarnings("unchecked") - public void proxiedListeners() { + void proxiedListeners() { MyOrderedListener1 listener1 = new MyOrderedListener1(); MyOrderedListener2 listener2 = new MyOrderedListener2(listener1); ApplicationListener proxy1 = (ApplicationListener) new ProxyFactory(listener1).getProxy(); @@ -262,7 +265,7 @@ public void proxiedListeners() { @Test @SuppressWarnings("unchecked") - public void proxiedListenersMixedWithTargetListeners() { + void proxiedListenersMixedWithTargetListeners() { MyOrderedListener1 listener1 = new MyOrderedListener1(); MyOrderedListener2 listener2 = new MyOrderedListener2(listener1); ApplicationListener proxy1 = (ApplicationListener) new ProxyFactory(listener1).getProxy(); @@ -281,7 +284,7 @@ public void proxiedListenersMixedWithTargetListeners() { /** * Regression test for issue 28283, - * where event listeners proxied due to e.g. + * where event listeners proxied due to, for example, *